Skip to content

Commit

Permalink
feat: export frontmatter metadata
Browse files Browse the repository at this point in the history
Export the variables defined in the frontmatter within a
`<script context="module">` block, so that it's accessible to tools like
Vite's `import.meta.glob('...', { import: 'metadata', eager: true })`.
  • Loading branch information
nvlang committed Jul 18, 2024
1 parent 9741f5e commit 82689b8
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 86 deletions.
70 changes: 51 additions & 19 deletions src/base/Sveltex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,24 +183,28 @@ export class Sveltex<
// append CoffeeScript code, which would (presumably) throw an error.
const lang = attributes['lang']?.toString().toLowerCase() ?? 'js';

if (this.mathPresent[filename]) {
script.push(...this._mathHandler.scriptLines);
}
if (this.codePresent[filename]) {
script.push(...this._codeHandler.scriptLines);
}
if (attributes['context'] === 'module') {
script.push(...(this.scriptModuleLines[filename] ?? []));
} else {
if (this.mathPresent[filename]) {
script.push(...this._mathHandler.scriptLines);
}
if (this.codePresent[filename]) {
script.push(...this._codeHandler.scriptLines);
}

// From frontmatter
script.push(...(this.scriptLines[filename] ?? []));
// From frontmatter
script.push(...(this.scriptLines[filename] ?? []));

script.push(
...detectAndImportComponents(
markup,
this._configuration.markdown.components,
content,
script,
),
);
script.push(
...detectAndImportComponents(
markup,
this._configuration.markdown.components,
content,
script,
),
);
}

if (script.length === 0) return;

Expand Down Expand Up @@ -257,11 +261,17 @@ export class Sveltex<

/**
* Lines to add to the `<script>` tag in the Svelte file, e.g. `import`
* statements for the TeX components or variable definitions from the
* frontmatter.
* statements for the TeX components.
*/
private scriptLines: Record<string, string[]> = {};

/**
* Lines to add to the `<script context="module">` tag in the Svelte file,
* e.g. `export const` statements for the metadata defined in the
* frontmatter.
*/
private scriptModuleLines: Record<string, string[]> = {};

/**
* @param content - The whole Svelte file content.
* @param filename - The filename of the Svelte file. (Isn't this actually a
Expand Down Expand Up @@ -313,6 +323,7 @@ export class Sveltex<
let headId: string | undefined = undefined;
let headSnippet: ProcessedSnippet | undefined = undefined;
let scriptPresent: boolean = false;
let scriptModulePresent: boolean = false;
const prependToProcessed: string[] = [];
let frontmatter: Frontmatter | undefined = undefined;

Expand Down Expand Up @@ -365,6 +376,8 @@ export class Sveltex<
headLines.push(...handledFrontmatter.headLines);
this.scriptLines[filename] =
handledFrontmatter.scriptLines;
this.scriptModuleLines[filename] =
handledFrontmatter.scriptModuleLines;
frontmatter = handledFrontmatter.frontmatter;
processedSnippet = {
processed: '',
Expand All @@ -381,9 +394,20 @@ export class Sveltex<
processed.startsWith(`<svelte${colonId}head`)
) {
headId = uuid;
} else if (
!scriptModulePresent &&
processed.startsWith('<script') &&
/^<script\s(?:[^>]*\s)?context=\s*(["'])module\1(?:\s[^>]*)?>/.test(
processed,
)
) {
scriptModulePresent = true;
} else if (
!scriptPresent &&
processed.startsWith('<script')
processed.startsWith('<script') &&
!/^<script\s(?:[^>]*\s)?context=\s*(["'])module\1(?:\s[^>]*)?>/.test(
processed,
)
) {
scriptPresent = true;
}
Expand Down Expand Up @@ -434,6 +458,14 @@ export class Sveltex<
}
}

// Add <script context="module"> tag if not present
if (!scriptModulePresent) {
prependToProcessed.push(
'<script context="module">',
'</script>',
);
}

// Add <script> tag if not present
if (!scriptPresent) {
prependToProcessed.push('<script>', '</script>');
Expand Down
10 changes: 7 additions & 3 deletions src/utils/frontmatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,17 +240,21 @@ function addMetaHttpEquiv(
export function handleFrontmatter(snippet: ProcessableSnippet<'frontmatter'>): {
headLines: string[];
scriptLines: string[];
scriptModuleLines: string[];
frontmatter: Frontmatter | undefined;
} {
const frontmatter = interpretFrontmatter(parseFrontmatter(snippet));
const headLines: string[] = [];
const scriptLines: string[] = [];
const scriptModuleLines: string[] = [];
if (frontmatter === undefined)
return { headLines, scriptLines, frontmatter };
return { headLines, scriptLines, scriptModuleLines, frontmatter };
const { title, base, noscript, link, meta, imports } = frontmatter;

Object.entries(frontmatter).forEach(([key, value]) => {
scriptLines.push(`const ${key} = ${JSON.stringify(value)};`);
scriptModuleLines.push(
`export const ${key} = ${JSON.stringify(value)};`,
);
});

// Imports
Expand Down Expand Up @@ -324,5 +328,5 @@ export function handleFrontmatter(snippet: ProcessableSnippet<'frontmatter'>): {
});
}

return { headLines, scriptLines, frontmatter };
return { headLines, scriptLines, scriptModuleLines, frontmatter };
}
10 changes: 9 additions & 1 deletion tests/unit/base/Sveltex/sveltex.commonmark.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ describe('CommonMark compliance', () => {
expect(actual).toBeDefined();
nodeAssert(actual !== undefined);
actual = actual.replace('<script>\n</script>\n', '');
actual = actual.replace(
'<script context="module">\n</script>\n',
'',
);
actual = normalizeHtml(actual, s.markdownBackend);
expect(actual).toEqual(expected);
vi.restoreAllMocks();
Expand Down Expand Up @@ -147,7 +151,7 @@ describe.concurrent('CommonMark compliance (with highlighters)', () => {
// (highlighting tags) = (original code, with special characters
// escaped)".
let actual = code
.replace(/<script>.*?<\/script>\n/s, '')
.replace(/<script[^>]*>.*?<\/script>\n/gs, '')
.replace(/<svelte:head>.*?<\/svelte:head>\n/s, '')
.replaceAll(/<\/?span[^>]*>/g, '')
.replaceAll(/class="(language-\S+)?.*?"/g, 'class="$1"')
Expand Down Expand Up @@ -237,6 +241,10 @@ describe('CommonMark non-compliance', () => {
expect(actual).toBeDefined();
nodeAssert(actual !== undefined);
actual = actual.replace('<script>\n</script>\n', '');
actual = actual.replace(
'<script context="module">\n</script>\n',
'',
);
actual = normalizeHtml(actual, s.markdownBackend);
expect(actual).not.toEqual(expected);
},
Expand Down
1 change: 1 addition & 0 deletions tests/unit/base/Sveltex/sveltex.edge-cases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ describe('specific examples', () => {
expect(result).toEqual(
sanitizeHtml(result, {
allowedTags: false,
allowedAttributes: false,
allowVulnerableTags: true,
}),
);
Expand Down
70 changes: 60 additions & 10 deletions tests/unit/base/Sveltex/sveltex.script.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ describe('Sveltex', () => {
filename: '90ed9f9c-b8b8-4a8a-aeee-1dc3cb412cc4.sveltex',
});
expect((markupOut as Processed).code).toMatch(
/<svelte:head>\n.{0,100}<link rel="stylesheet" href=".{0,100}\.css">\n.{0,100}<\/svelte:head>\n<script>\n<\/script>\n.{0,100}<figure>\n<svelte:component this={Sveltex__tex__something} \/>\n<\/figure>\n.{0,100}<p><code>code<\/code>.{0,100}\n<span class="katex">/s,
/<svelte:head>\n.{0,100}<link rel="stylesheet" href=".{0,100}\.css">\n.{0,100}<\/svelte:head>\n<script context="module">\n<\/script>\n<script>\n<\/script>\n.{0,100}<figure>\n<svelte:component this={Sveltex__tex__something} \/>\n<\/figure>\n.{0,100}<p><code>code<\/code>.{0,100}\n<span class="katex">/s,
);
const scriptOut = await sp.script({
content: '',
Expand All @@ -113,7 +113,7 @@ describe('Sveltex', () => {
content: 'something',
filename: '7a541239-3058-460b-b3c6-5076a2f3f73b.sveltex',
});
expect((markupOut as Processed).code).toEqual(
expect((markupOut as Processed).code).toContain(
'<script>\n</script>\n<p>something</p>',
);

Expand Down Expand Up @@ -150,8 +150,17 @@ describe('Sveltex', () => {
markup: markupOut?.code ?? '',
filename: '9ae17b43-d19c-4ca3-9772-36e506ffb4a5.sveltex',
});
expect((scriptOut as Processed).code).toEqual(
'\nimport Sveltex__tex__ref_without_quotation_marks from \'/src/sveltex/tex/ref-without-quotation-marks.svelte\';\nconst foo = "bar";\nconst author = "Jane Doe";\nconst title = "Example";\nconst meta = [{"name":"author","content":"Jane Doe"}];\n',
expect((scriptOut as Processed).code).toContain(
"\nimport Sveltex__tex__ref_without_quotation_marks from '/src/sveltex/tex/ref-without-quotation-marks.svelte';\n",
);
const scriptModuleOut = await sp.script({
content: '',
attributes: { context: 'module' },
markup: markupOut?.code ?? '',
filename: '9ae17b43-d19c-4ca3-9772-36e506ffb4a5.sveltex',
});
expect((scriptModuleOut as Processed).code).toContain(
'export const foo = "bar";\nexport const author = "Jane Doe";\nexport const title = "Example";\nexport const meta = [{"name":"author","content":"Jane Doe"}];\n',
);

existsSync.mockReset();
Expand All @@ -174,7 +183,7 @@ describe('Sveltex', () => {
markup: markupOut?.code ?? '',
filename: '420274ac-0f4d-49b9-842e-f9937ae45ca6.sveltex',
});
expect((scriptOut as Processed).code).toEqual(
expect((scriptOut as Processed).code).toContain(
"\n```\nimport Sveltex__tex__ref_without_quotation_marks from '/src/sveltex/tex/ref-without-quotation-marks.svelte';\n```\n",
);

Expand All @@ -187,7 +196,7 @@ describe('Sveltex', () => {
content: '<Example />',
filename: '6f85b451-6ae9-42c4-a03b-cca772ef7455.sveltex',
});
expect((markupOut as Processed).code).toEqual(
expect((markupOut as Processed).code).toContain(
'<script>\n</script>\n<Example />',
);

Expand All @@ -197,7 +206,7 @@ describe('Sveltex', () => {
markup: markupOut?.code ?? '',
filename: '6f85b451-6ae9-42c4-a03b-cca772ef7455.sveltex',
});
expect((scriptOut as Processed).code).toEqual(
expect((scriptOut as Processed).code).toContain(
"\nimport Example from '$lib/components/Example.svelte';\n",
);
existsSync.mockReset();
Expand All @@ -210,7 +219,7 @@ describe('Sveltex', () => {
'<script>\nconst something = 0;\n</script>\n<Example />',
filename: 'e2468a1e-9389-4537-b6ee-ffd9b4499c4b.sveltex',
});
expect((markupOut as Processed).code).toEqual(
expect((markupOut as Processed).code).toContain(
'<script>\nconst something = 0;\n</script>\n<Example />',
);

Expand All @@ -220,7 +229,7 @@ describe('Sveltex', () => {
markup: markupOut?.code ?? '',
filename: 'e2468a1e-9389-4537-b6ee-ffd9b4499c4b.sveltex',
});
expect((scriptOut as Processed).code).toEqual(
expect((scriptOut as Processed).code).toContain(
"\nconst something = 0;\n\nimport Example from '$lib/components/Example.svelte';\n",
);
existsSync.mockReset();
Expand All @@ -233,7 +242,7 @@ describe('Sveltex', () => {
"<script>\nimport Example from '$lib/components/Example.svelte';\n</script>\n<Example />",
filename: '6f85b451-6ae9-42c4-a03b-cca772ef7455.sveltex',
});
expect((markupOut as Processed).code).toEqual(
expect((markupOut as Processed).code).toContain(
"<script>\nimport Example from '$lib/components/Example.svelte';\n</script>\n<Example />",
);

Expand All @@ -245,6 +254,47 @@ describe('Sveltex', () => {
filename: '6f85b451-6ae9-42c4-a03b-cca772ef7455.sveltex',
});
expect(scriptOut).toBeUndefined();

const scriptModuleOut = await sp.script({
content: '\n',
attributes: { context: 'module' },
markup: markupOut?.code ?? '',
filename: '6f85b451-6ae9-42c4-a03b-cca772ef7455.sveltex',
});
expect(scriptModuleOut).toBeUndefined();
existsSync.mockReset();
});

it('auto-importing some components (already imported in frontmatter)', async () => {
existsSync.mockReturnValue(true);
const markupOut = await sp.markup({
content:
'---\nimports:\n- $lib/components/Example.svelte: Example;\n---\n<script context="module">\n</script>\n\n<Example />',
filename: 'a0dcf7dd-cabd-4816-a963-c30fc654ff34.sveltex',
});
expect((markupOut as Processed).code).toEqual(
'<script>\n</script>\n\n<script context="module">\n</script>\n<Example />',
);

const scriptOut = await sp.script({
content: '\n',
attributes: {},
markup: markupOut?.code ?? '',
filename: 'a0dcf7dd-cabd-4816-a963-c30fc654ff34.sveltex',
});
expect((scriptOut as Processed).code).toMatch(
/^\s*import Example from '\$lib\/components\/Example.svelte';\s*$/,
);

const scriptModuleOut = await sp.script({
content: '\n',
attributes: { context: 'module' },
markup: markupOut?.code ?? '',
filename: 'a0dcf7dd-cabd-4816-a963-c30fc654ff34.sveltex',
});
expect((scriptModuleOut as Processed).code).toContain(
'export const imports = [{"$lib/components/Example.svelte":"Example;"}];',
);
existsSync.mockReset();
});
});
Expand Down
Loading

0 comments on commit 82689b8

Please sign in to comment.