add a vim-like command line under :
This commit is contained in:
parent
0ce7f50285
commit
9cac6c3c3e
20
content/treehouse/cmd.tree
Normal file
20
content/treehouse/cmd.tree
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
%% title = "command line"
|
||||||
|
|
||||||
|
% id = "01JEK4XKK26T6W603FTPQHQ7C8"
|
||||||
|
- press `<kbd>:</kbd>`{=html} to open the command line.
|
||||||
|
|
||||||
|
% id = "01JEK4XKK27KXP01EK8K890SPK"
|
||||||
|
- type in your command, then press `<kbd>Enter</kbd>`{=html} to run it.
|
||||||
|
|
||||||
|
- `<kbd>Esc</kbd>`{=html} closes the command line.
|
||||||
|
|
||||||
|
- `<kbd>Tab</kbd>`{=html} cycles through suggestions.
|
||||||
|
|
||||||
|
- you may also use the mouse to close the command line or pick a suggestion from the list.
|
||||||
|
|
||||||
|
% id = "01JEK4XKK2EDTVCNZQRV9XDZXJ"
|
||||||
|
- unknown commands do not do anything.
|
||||||
|
known commands usually result in immediate feedback.
|
||||||
|
|
||||||
|
% id = "01JEK4XKK2S4W0TPT4JY8AH143"
|
||||||
|
- the command line is currently not accessible on mobile devices.
|
|
@ -1,5 +1,5 @@
|
||||||
%% title = "developer tools"
|
%% title = "developer tools"
|
||||||
styles = ["page/treehouse/dev/tools.css"]
|
styles = ["dev.css"]
|
||||||
scripts = ["treehouse/dev/picture-upload.js"]
|
scripts = ["treehouse/dev/picture-upload.js"]
|
||||||
|
|
||||||
% id = "01JEHDJSJP282VCTRKYHNFM4N7"
|
% id = "01JEHDJSJP282VCTRKYHNFM4N7"
|
||||||
|
|
|
@ -1,18 +1,14 @@
|
||||||
|
/* Styles for developer tools.
|
||||||
|
This stylesheet MUST NOT be used for modifying the appearance of elements globally.
|
||||||
|
If you notice that it is for whatever reason, please bonk liquidex on the head. */
|
||||||
|
|
||||||
th-picture-upload {
|
th-picture-upload {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
border: 1px solid var(--border-1);
|
|
||||||
padding: 8px 12px;
|
|
||||||
margin-right: 8px;
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
|
||||||
&:focus {
|
|
||||||
border-color: var(--liquidex-brand-blue);
|
|
||||||
}
|
|
||||||
|
|
||||||
& > .nothing-pasted {
|
& > .nothing-pasted {
|
||||||
|
border: 1px solid var(--border-1);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
opacity: 50%;
|
opacity: 50%;
|
||||||
padding: 16px;
|
padding: 16px;
|
|
@ -108,6 +108,11 @@ body {
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:focus-visible {
|
||||||
|
outline: 1px solid var(--liquidex-brand-blue);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Set up typography */
|
/* Set up typography */
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
|
@ -131,8 +136,11 @@ pre,
|
||||||
code,
|
code,
|
||||||
kbd,
|
kbd,
|
||||||
button,
|
button,
|
||||||
select {
|
select,
|
||||||
|
input,
|
||||||
|
dfn {
|
||||||
font-family: "RecVar", sans-serif;
|
font-family: "RecVar", sans-serif;
|
||||||
|
font-style: normal;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,8 +152,9 @@ pre,
|
||||||
code,
|
code,
|
||||||
kbd,
|
kbd,
|
||||||
button,
|
button,
|
||||||
select {
|
select,
|
||||||
font-size: 100%;
|
input {
|
||||||
|
font-size: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
@ -499,28 +508,7 @@ h1.page-title {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Style the `new` link on the homepage */
|
/* Style badges */
|
||||||
a[data-cast~="new"] {
|
|
||||||
flex-shrink: 0;
|
|
||||||
color: var(--text-color);
|
|
||||||
opacity: 50%;
|
|
||||||
|
|
||||||
&.has-news {
|
|
||||||
opacity: 100%;
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
& .new-text {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& .badge {
|
|
||||||
margin-left: var(--8px);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Style new badges */
|
|
||||||
span.badge {
|
span.badge {
|
||||||
--recursive-wght: 800;
|
--recursive-wght: 800;
|
||||||
--recursive-mono: 1;
|
--recursive-mono: 1;
|
||||||
|
@ -641,6 +629,20 @@ footer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Style dialogues */
|
||||||
|
|
||||||
|
dialog[open] {
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: var(--background-color);
|
||||||
|
border: 1px solid var(--border-1);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Style emojis to be readable */
|
/* Style emojis to be readable */
|
||||||
|
|
||||||
img[data-cast~="emoji"] {
|
img[data-cast~="emoji"] {
|
||||||
|
@ -702,35 +704,78 @@ th-emoji-tooltip p {
|
||||||
cursor: help;
|
cursor: help;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Funny joke */
|
/* Command line */
|
||||||
|
|
||||||
@keyframes hello-there {
|
th-command-line {
|
||||||
0% {
|
--recursive-mono: 1;
|
||||||
opacity: 0%;
|
--recursive-casl: 0;
|
||||||
}
|
|
||||||
|
|
||||||
70% {
|
|
||||||
opacity: 0%;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 70%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.oops-you-seem-to-have-gotten-stuck {
|
|
||||||
margin-top: 16px;
|
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
flex-direction: column;
|
||||||
opacity: 0%;
|
|
||||||
|
background-color: var(--background-color-tooltip);
|
||||||
|
font-size: 87.5%;
|
||||||
|
|
||||||
|
&.visible {
|
||||||
|
display: flex;
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#index\:treehouse
|
& > .input-wrapper {
|
||||||
> details:not([open])
|
display: flex;
|
||||||
> summary
|
flex-direction: row;
|
||||||
.oops-you-seem-to-have-gotten-stuck {
|
|
||||||
display: inline;
|
padding: 2px 4px;
|
||||||
animation: 4s hello-there forwards;
|
width: 100%;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: ":";
|
||||||
|
padding-right: 2px;
|
||||||
|
opacity: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > input {
|
||||||
|
background: none;
|
||||||
|
color: var(--text-color);
|
||||||
|
border: none;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > ul.suggestions {
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
& > li {
|
||||||
|
padding: 2px 8px;
|
||||||
|
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
& > dfn {
|
||||||
|
--recursive-crsv: 0;
|
||||||
|
--recursive-wght: 700;
|
||||||
|
margin-right: 2ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.tabbed {
|
||||||
|
background-color: var(--liquidex-brand-blue);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Literate programming support */
|
/* Literate programming support */
|
||||||
|
|
174
static/js/command-line.js
Normal file
174
static/js/command-line.js
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
export class CommandLine extends HTMLElement {
|
||||||
|
static commands = new Map();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.suggestions = this.appendChild(document.createElement("ul"));
|
||||||
|
this.suggestions.classList.add("suggestions");
|
||||||
|
|
||||||
|
let inputWrapper = this.appendChild(document.createElement("div"));
|
||||||
|
inputWrapper.classList.add("input-wrapper");
|
||||||
|
this.input = inputWrapper.appendChild(document.createElement("input"));
|
||||||
|
this.input.type = "text";
|
||||||
|
|
||||||
|
window.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key == ":" && !this.visible) {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
this.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key == "Escape") {
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("click", () => {
|
||||||
|
this.hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addEventListener("click", (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.input.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key == "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
this.hide();
|
||||||
|
this.runCommand(this.input.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key == "Tab") {
|
||||||
|
event.preventDefault();
|
||||||
|
if (event.shiftKey) this.tabToPreviousSuggestion();
|
||||||
|
else this.tabToNextSuggestion();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.input.addEventListener("input", () => {
|
||||||
|
this.updateSuggestions();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get visible() {
|
||||||
|
return this.classList.contains("visible");
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
this.classList.add("visible");
|
||||||
|
this.input.focus();
|
||||||
|
this.input.value = "";
|
||||||
|
this.updateSuggestions();
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.classList.remove("visible");
|
||||||
|
}
|
||||||
|
|
||||||
|
tab(current, next) {
|
||||||
|
current?.classList?.remove("tabbed");
|
||||||
|
next.classList.add("tabbed");
|
||||||
|
|
||||||
|
this.input.value = next.name;
|
||||||
|
// NOTE: Do NOT update suggestions here.
|
||||||
|
// This would cause the tabbing to break.
|
||||||
|
}
|
||||||
|
|
||||||
|
tabToNextSuggestion() {
|
||||||
|
let current = this.suggestions.querySelector(".tabbed");
|
||||||
|
let next = current?.nextSibling ?? this.suggestions.childNodes[0];
|
||||||
|
this.tab(current, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
tabToPreviousSuggestion() {
|
||||||
|
let current = this.suggestions.querySelector(".tabbed");
|
||||||
|
let previous =
|
||||||
|
current?.previousSibling ??
|
||||||
|
this.suggestions.childNodes[this.suggestions.childNodes.length - 1];
|
||||||
|
this.tab(current, previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSuggestions() {
|
||||||
|
let search = parseCommand(this.input.value)?.command ?? "";
|
||||||
|
let suggestions = Array.from(CommandLine.commands.entries()).filter(
|
||||||
|
([name, def]) => !def.isAlias && fuzzyMatch(search, name),
|
||||||
|
);
|
||||||
|
suggestions.sort();
|
||||||
|
|
||||||
|
this.suggestions.replaceChildren();
|
||||||
|
for (let [name, def] of suggestions) {
|
||||||
|
let suggestion = this.suggestions.appendChild(document.createElement("li"));
|
||||||
|
let commandName = suggestion.appendChild(document.createElement("dfn"));
|
||||||
|
commandName.textContent = name;
|
||||||
|
let commandDescription = suggestion.appendChild(document.createElement("span"));
|
||||||
|
commandDescription.classList.add("description");
|
||||||
|
commandDescription.textContent = def.description;
|
||||||
|
|
||||||
|
suggestion.name = name;
|
||||||
|
suggestion.def = def;
|
||||||
|
|
||||||
|
suggestion.addEventListener("click", () => {
|
||||||
|
this.input.value = name;
|
||||||
|
this.updateSuggestions();
|
||||||
|
this.input.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runCommand(commandLine) {
|
||||||
|
let { command, args } = parseCommand(commandLine);
|
||||||
|
let commandDef = CommandLine.commands.get(command);
|
||||||
|
if (CommandLine.commands.has(command)) {
|
||||||
|
commandDef.run(args);
|
||||||
|
} else {
|
||||||
|
console.log(`unknown command`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static registerCommand({ aliases, description, run }) {
|
||||||
|
for (let i = 0; i < aliases.length; ++i) {
|
||||||
|
CommandLine.commands.set(aliases[i], {
|
||||||
|
isAlias: i != 0,
|
||||||
|
description,
|
||||||
|
run,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("th-command-line", CommandLine);
|
||||||
|
|
||||||
|
function parseCommand(commandLine) {
|
||||||
|
let result = /^([^ ]+) *(.*)$/.exec(commandLine);
|
||||||
|
if (result == null) return null;
|
||||||
|
|
||||||
|
let [_, command, args] = result;
|
||||||
|
return { command, args };
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.forrestthewoods.com/blog/reverse_engineering_sublime_texts_fuzzy_match/
|
||||||
|
function fuzzyMatch(pattern, string) {
|
||||||
|
let iPattern = 0;
|
||||||
|
let iString = 0;
|
||||||
|
|
||||||
|
while (iPattern < pattern.length && iString < string.length) {
|
||||||
|
if (pattern.charAt(iPattern).toLowerCase() == string.charAt(iString).toLowerCase()) {
|
||||||
|
iPattern += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
iString += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return iPattern == pattern.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
CommandLine.registerCommand({
|
||||||
|
aliases: ["help", "h"],
|
||||||
|
description: '"OwO, what is this?"',
|
||||||
|
run() {
|
||||||
|
window.location = `${TREEHOUSE_SITE}/treehouse/cmd`;
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,10 +1,11 @@
|
||||||
|
import { CommandLine } from "treehouse/command-line.js";
|
||||||
|
|
||||||
class PictureUpload extends HTMLElement {
|
class PictureUpload extends HTMLElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.tabIndex = 0;
|
|
||||||
this.gotoInit();
|
this.gotoInit();
|
||||||
|
|
||||||
this.preview = this.querySelector("img[name='preview']");
|
this.preview = this.querySelector("img[name='preview']");
|
||||||
|
@ -30,16 +31,17 @@ class PictureUpload extends HTMLElement {
|
||||||
gotoInit() {
|
gotoInit() {
|
||||||
this.setState("init");
|
this.setState("init");
|
||||||
this.innerHTML = `
|
this.innerHTML = `
|
||||||
<div class="nothing-pasted">
|
<div class="nothing-pasted" tabindex="0">
|
||||||
paste or drop an image here to make a picture out of it
|
paste or drop an image here to make a picture out of it
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
this.querySelector(".nothing-pasted").focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
async gotoHavePicture(imageType, imageFile) {
|
async gotoHavePicture(imageType, imageFile) {
|
||||||
this.setState("have-picture");
|
this.setState("have-picture");
|
||||||
this.innerHTML = `
|
this.innerHTML = `
|
||||||
<div class="have-picture">
|
<form name="upload" class="have-picture">
|
||||||
<img name="preview" class="pic" alt="preview">
|
<img name="preview" class="pic" alt="preview">
|
||||||
<p>
|
<p>
|
||||||
<span name="preview-width"></span> × <span name="preview-height"></span> px (<span name="file-size"></span>)
|
<span name="preview-width"></span> × <span name="preview-height"></span> px (<span name="file-size"></span>)
|
||||||
|
@ -57,19 +59,20 @@ class PictureUpload extends HTMLElement {
|
||||||
</select>
|
</select>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<button name="upload">upload</button>
|
<button type="submit" name="upload">upload</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
let uploadForm = this.querySelector("form[name='upload']");
|
||||||
let preview = this.querySelector("img[name='preview']");
|
let preview = this.querySelector("img[name='preview']");
|
||||||
let previewWidth = this.querySelector("[name='preview-width']");
|
let previewWidth = this.querySelector("[name='preview-width']");
|
||||||
let previewHeight = this.querySelector("[name='preview-height']");
|
let previewHeight = this.querySelector("[name='preview-height']");
|
||||||
let fileSize = this.querySelector("[name='file-size']");
|
let fileSize = this.querySelector("[name='file-size']");
|
||||||
let label = this.querySelector("[name='label']");
|
let label = this.querySelector("[name='label']");
|
||||||
let compression = this.querySelector("[name='compression']");
|
let compression = this.querySelector("[name='compression']");
|
||||||
let upload = this.querySelector("button[name='upload']");
|
|
||||||
|
|
||||||
fileSize.textContent = formatSizeSI(imageFile.size);
|
fileSize.textContent = formatSizeSI(imageFile.size);
|
||||||
|
label.focus();
|
||||||
|
|
||||||
let url = URL.createObjectURL(imageFile);
|
let url = URL.createObjectURL(imageFile);
|
||||||
preview.src = url;
|
preview.src = url;
|
||||||
|
@ -80,7 +83,9 @@ class PictureUpload extends HTMLElement {
|
||||||
previewHeight.textContent = bitmap.height.toString();
|
previewHeight.textContent = bitmap.height.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
upload.addEventListener("click", async () => {
|
uploadForm.addEventListener("submit", async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
let params = new URLSearchParams({
|
let params = new URLSearchParams({
|
||||||
label: label.value,
|
label: label.value,
|
||||||
format: imageType,
|
format: imageType,
|
||||||
|
@ -147,3 +152,21 @@ function formatSizeSI(bytes) {
|
||||||
unitDisplay: "narrow",
|
unitDisplay: "narrow",
|
||||||
}).format(bytes);
|
}).format(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (TREEHOUSE_DEV) {
|
||||||
|
CommandLine.registerCommand({
|
||||||
|
aliases: ["addpic"],
|
||||||
|
description: "add a picture interactively and copy its ulid",
|
||||||
|
run() {
|
||||||
|
let dialog = document.body.appendChild(document.createElement("dialog"));
|
||||||
|
dialog.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key == "Escape") dialog.close();
|
||||||
|
});
|
||||||
|
dialog.addEventListener("close", () => {
|
||||||
|
dialog.remove();
|
||||||
|
});
|
||||||
|
dialog.appendChild(new PictureUpload());
|
||||||
|
dialog.show();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<th-emoji-tooltips></th-emoji-tooltips>
|
<th-emoji-tooltips></th-emoji-tooltips>
|
||||||
|
<th-command-line></th-command-line>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -19,28 +19,32 @@ clever to do while browser vendors figure that out, we'll just have to do a cach
|
||||||
|
|
||||||
{{#if dev}}
|
{{#if dev}}
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import "treehouse/live-reload.js";
|
import "treehouse/dev/live-reload.js";
|
||||||
|
import "treehouse/dev/picture-upload.js";
|
||||||
</script>
|
</script>
|
||||||
|
<link rel="stylesheet" href="{{ asset 'css/dev.css' }}">
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
const TREEHOUSE_DEV = {{ dev }};
|
||||||
const TREEHOUSE_SITE = `{{ config.site }}`;
|
const TREEHOUSE_SITE = `{{ config.site }}`;
|
||||||
|
|
||||||
{{!-- Yeah, this should probably be solved in a better way somehow.
|
{{!-- Yeah, this should probably be solved in a better way somehow.
|
||||||
For now this is used to allow literate-programming.js to refer to syntax files with the ?cache attribute,
|
For now this is used to allow literate-programming.js to refer to syntax files with the ?v attribute,
|
||||||
so that they don't need to be redownloaded every single time. --}}
|
so that they don't need to be redownloaded every single time. --}}
|
||||||
const TREEHOUSE_SYNTAX_URLS = {
|
const TREEHOUSE_SYNTAX_URLS = {
|
||||||
javascript: `{{{ asset 'syntax/javascript.json' }}}`,
|
javascript: `{{{ asset 'syntax/javascript.json' }}}`,
|
||||||
haku: `{{{ asset 'syntax/haku.json' }}}`,
|
haku: `{{{ asset 'syntax/haku.json' }}}`,
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<script type="module">
|
<script type="module" async>
|
||||||
import "treehouse/spells.js";
|
import "treehouse/spells.js";
|
||||||
import "treehouse/ulid.js";
|
import "treehouse/ulid.js";
|
||||||
import "treehouse/usability.js";
|
import "treehouse/usability.js";
|
||||||
import "treehouse/settings.js";
|
import "treehouse/settings.js";
|
||||||
import "treehouse/tree.js";
|
import "treehouse/tree.js";
|
||||||
import "treehouse/emoji.js";
|
import "treehouse/emoji.js";
|
||||||
|
import "treehouse/command-line.js";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<meta property="og:site_name" content="{{ config.user.title }}">
|
<meta property="og:site_name" content="{{ config.user.title }}">
|
||||||
|
|
Loading…
Reference in a new issue