HTML and Svelte
A tag's name is coloured as a string and its attributes as functions, as shiki's HTML grammar
colours them. The body of a <script> is coloured as JS and a <style> as CSS, so a
page's embedded code reads like a file of its own.
Svelte adds {…}, which is JS wherever it appears, in text or as an attribute's value. A block
like {#if x} or {:else} colours its tag as a keyword.
import { css } from "./css.js";
import { js } from "./js.js";
import { type Span, shift } from "./scan.js";
/** The end of a `{…}` that opens at `from`, past its matching `}`. */
function braceEnd(source: string, from: number): number {
let depth = 0;
for (let i = from; i < source.length; i++) {
const c = source[i];
if (c === '"' || c === "'" || c === "`") {
const close = source.indexOf(c, i + 1);
if (close === -1) return source.length;
i = close;
} else if (c === "{") depth++;
else if (c === "}" && --depth === 0) return i + 1;
}
return source.length;
}
/** A Svelte expression, `{` and `}` included. */
function expression(source: string, start: number, end: number): Span[] {
const inner = source.slice(start + 1, end - 1);
const tag = /^[#:/@][\w-]+/.exec(inner)?.[0] ?? "";
// The block's word is the keyword. Its `#`, `:` or `/` stays plain, as shiki leaves it.
const spans: Span[] = tag
? [{ colour: "keyword", start: start + 2, end: start + 1 + tag.length }]
: [];
return spans.concat(shift(js(inner.slice(tag.length)), start + 1 + tag.length));
}
export function markup(source: string, svelte = false): Span[] {
const spans: Span[] = [];
let i = 0;
while (i < source.length) {
const c = source[i];
if (source.startsWith("<!--", i)) {
const close = source.indexOf("-->", i + 4);
const end = close === -1 ? source.length : close + 3;
spans.push({ colour: "comment", start: i, end });
i = end;
} else if (c === "<" && /[a-zA-Z!/]/.test(source[i + 1] ?? "")) {
i = tag(source, i, svelte, spans);
} else if (svelte && c === "{") {
const end = braceEnd(source, i);
spans.push(...expression(source, i, end));
i = end;
} else {
i++;
}
}
return spans;
}
/** A script's code. In Svelte, a rune's `$`, as in `$props()`, stays plain, as shiki leaves it. */
function script(code: string, svelte: boolean): Span[] {
const spans = js(code);
if (!svelte) return spans;
return spans.map((s) =>
s.colour === "function" && code[s.start] === "$" ? { ...s, start: s.start + 1 } : s,
);
}
/** Colours the tag at `from`, and the script or style it opens. Returns where to go on from. */
function tag(source: string, from: number, svelte: boolean, spans: Span[]): number {
const name = /^<\/?!?([\w:-]+)/.exec(source.slice(from, from + 100));
if (!name) return from + 1;
const nameStart = from + name[0].length - name[1]!.length;
spans.push({ colour: "string", start: nameStart, end: from + name[0].length });
let i = from + name[0].length;
while (i < source.length && source[i] !== ">") {
const c = source[i]!;
if (svelte && c === "{") {
const end = braceEnd(source, i);
spans.push(...expression(source, i, end));
i = end;
} else if (c === '"' || c === "'") {
const close = source.indexOf(c, i + 1);
const end = close === -1 ? source.length : close + 1;
spans.push({ colour: "string", start: i, end });
i = end;
} else if (c === "=") {
spans.push({ colour: "keyword", start: i, end: i + 1 });
i++;
} else if (/[^\s/>]/.test(c)) {
const attr = /^[^\s=>/"']+/.exec(source.slice(i, i + 200))?.[0] ?? c;
spans.push({ colour: "function", start: i, end: i + attr.length });
i += attr.length;
} else {
i++;
}
}
i++;
const element = name[1]!.toLowerCase();
if (name[0][1] !== "/" && (element === "script" || element === "style")) {
const close = source.indexOf(`</${element}`, i);
const end = close === -1 ? source.length : close;
const body = source.slice(i, end);
spans.push(...shift(element === "script" ? script(body, svelte) : css(body), i));
return end;
}
return i;
}