Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/markdown/remark/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"build": "astro-scripts build \"src/**/*.ts\" && tsc -p tsconfig.json",
"build:ci": "astro-scripts build \"src/**/*.ts\"",
"dev": "astro-scripts dev \"src/**/*.ts\"",
"test": "astro-scripts test \"test/**/*.test.js\""
"test": "astro-scripts test \"test/**/*.test.ts\"",
"typecheck:tests": "tsc -p tsconfig.test.json --noEmit"
},
"dependencies": {
"@astrojs/internal-helpers": "workspace:*",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import assert from 'node:assert/strict';
import { before, describe, it } from 'node:test';
import { createMarkdownProcessor } from '../dist/index.js';
import { createMarkdownProcessor, type MarkdownProcessor } from '../dist/index.js';

describe('autolinking', () => {
describe('plain md', () => {
let processor;
let processor: MarkdownProcessor;

before(async () => {
processor = await createMarkdownProcessor();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe('Bundle for browsers', async () => {
assert.ok(result.outputFiles.length > 0);
} catch (error) {
// Capture any esbuild errors and fail the test
assert.fail(error.message);
assert.fail((error as Error).message);
}
});
});
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import assert from 'node:assert/strict';
import { before, describe, it } from 'node:test';
import { createMarkdownProcessor } from '../dist/index.js';
import { createMarkdownProcessor, type MarkdownProcessor } from '../dist/index.js';

describe('entities', async () => {
let processor;
let processor: MarkdownProcessor;

before(async () => {
processor = await createMarkdownProcessor();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { extractFrontmatter, parseFrontmatter } from '../dist/index.js';
import {
extractFrontmatter,
parseFrontmatter,
type ParseFrontmatterOptions,
} from '../dist/index.js';

type FrontmatterStyle = ParseFrontmatterOptions['frontmatter'];

const bom = '\uFEFF';

Expand Down Expand Up @@ -157,13 +163,14 @@ describe('parseFrontmatter', () => {

it('frontmatter style for YAML', () => {
const yaml = `\nfoo: bar\n`;
const parse1 = (style) => parseFrontmatter(`---${yaml}---`, { frontmatter: style }).content;
const parse1 = (style: FrontmatterStyle) =>
parseFrontmatter(`---${yaml}---`, { frontmatter: style }).content;
assert.deepEqual(parse1('preserve'), `---${yaml}---`);
assert.deepEqual(parse1('remove'), '');
assert.deepEqual(parse1('empty-with-spaces'), ` \n \n `);
assert.deepEqual(parse1('empty-with-lines'), `\n\n`);

const parse2 = (style) =>
const parse2 = (style: FrontmatterStyle) =>
parseFrontmatter(`\n \n---${yaml}---\n\ncontent`, { frontmatter: style }).content;
assert.deepEqual(parse2('preserve'), `\n \n---${yaml}---\n\ncontent`);
assert.deepEqual(parse2('remove'), '\n \n\n\ncontent');
Expand All @@ -173,13 +180,14 @@ describe('parseFrontmatter', () => {

it('frontmatter style for TOML', () => {
const toml = `\nfoo = "bar"\n`;
const parse1 = (style) => parseFrontmatter(`+++${toml}+++`, { frontmatter: style }).content;
const parse1 = (style: FrontmatterStyle) =>
parseFrontmatter(`+++${toml}+++`, { frontmatter: style }).content;
assert.deepEqual(parse1('preserve'), `+++${toml}+++`);
assert.deepEqual(parse1('remove'), '');
assert.deepEqual(parse1('empty-with-spaces'), ` \n \n `);
assert.deepEqual(parse1('empty-with-lines'), `\n\n`);

const parse2 = (style) =>
const parse2 = (style: FrontmatterStyle) =>
parseFrontmatter(`\n \n+++${toml}+++\n\ncontent`, { frontmatter: style }).content;
assert.deepEqual(parse2('preserve'), `\n \n+++${toml}+++\n\ncontent`);
assert.deepEqual(parse2('remove'), '\n \n\n\ncontent');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { fileURLToPath } from 'node:url';
import { createMarkdownProcessor } from '../dist/index.js';
import type { VFile } from 'vfile';
import { createMarkdownProcessor, type RemarkPlugin } from '../dist/index.js';

describe('plugins', () => {
it('should be able to get file path when passing fileURL', async () => {
let context;
let context: VFile | undefined;

const collectFile: RemarkPlugin = () => (_tree, file) => {
context = file;
};

const processor = await createMarkdownProcessor({
remarkPlugins: [
() => {
const transformer = (_tree, file) => {
context = file;
};
return transformer;
},
],
remarkPlugins: [collectFile],
});

await processor.render(`test`, {
fileURL: new URL('virtual.md', import.meta.url),
});

assert.ok(typeof context === 'object');
assert.ok(context);
assert.equal(context.path, fileURLToPath(new URL('virtual.md', import.meta.url)));
});
});
Original file line number Diff line number Diff line change
@@ -1,33 +1,36 @@
import assert from 'node:assert/strict';
import { before, describe, it } from 'node:test';
import { visit } from 'unist-util-visit';
import { createMarkdownProcessor } from '../dist/index.js';
import {
createMarkdownProcessor,
type MarkdownProcessor,
type RehypePlugin,
} from '../dist/index.js';

describe('collect images', async () => {
let processor;
let processorWithHastProperties;
let processor: MarkdownProcessor;
let processorWithHastProperties: MarkdownProcessor;

before(async () => {
processor = await createMarkdownProcessor({ image: { domains: ['example.com'] } });

const addImageProps: RehypePlugin = () => (tree) => {
visit(tree, 'element', (node) => {
if (node.tagName === 'img') {
node.properties.className = ['image-class'];
node.properties.htmlFor = 'some-id';
}
});
};

processorWithHastProperties = await createMarkdownProcessor({
rehypePlugins: [
() => {
return (tree) => {
visit(tree, 'element', (node) => {
if (node.tagName === 'img') {
node.properties.className = ['image-class'];
node.properties.htmlFor = 'some-id';
}
});
};
},
],
rehypePlugins: [addImageProps],
});
});

it('should collect inline image paths', async () => {
const markdown = `Hello ![inline image url](./img.png)`;
const fileURL = 'file.md';
const fileURL = new URL('file.md', import.meta.url);

const {
code,
Expand All @@ -45,7 +48,7 @@ describe('collect images', async () => {

it('should collect allowed remote image paths', async () => {
const markdown = `Hello ![inline remote image url](https://example.com/example.png)`;
const fileURL = 'file.md';
const fileURL = new URL('file.md', import.meta.url);

const {
code,
Expand All @@ -62,7 +65,7 @@ describe('collect images', async () => {

it('should not collect other remote image paths', async () => {
const markdown = `Hello ![inline remote image url](https://google.com/google.png)`;
const fileURL = 'file.md';
const fileURL = new URL('file.md', import.meta.url);

const {
code,
Expand All @@ -79,7 +82,7 @@ describe('collect images', async () => {

it('should add image paths from definition', async () => {
const markdown = `Hello ![image ref][img-ref] ![remote image ref][remote-img-ref]\n\n[img-ref]: ./img.webp\n[remote-img-ref]: https://example.com/example.jpg`;
const fileURL = 'file.md';
const fileURL = new URL('file.md', import.meta.url);

const { code, metadata } = await processor.render(markdown, { fileURL });

Expand All @@ -94,7 +97,7 @@ describe('collect images', async () => {

it('should preserve className as HTML class attribute', async () => {
const markdown = `Hello ![image with class](./img.png)`;
const fileURL = 'file.md';
const fileURL = new URL('file.md', import.meta.url);

const { code } = await processorWithHastProperties.render(markdown, { fileURL });

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { createMarkdownProcessor, createShikiHighlighter } from '../dist/index.js';
import type { Element } from 'hast';
import type { LanguageRegistration, ThemeRegistration } from 'shiki';
import {
createMarkdownProcessor,
createShikiHighlighter,
type ShikiHighlighter,
} from '../dist/index.js';
// @ts-expect-error: `clearShikiHighlighterCache` is marked `@internal` and stripped from the `.d.ts`, but still exists at runtime.
import { clearShikiHighlighterCache } from '../dist/shiki.js';

describe('shiki syntax highlighting', () => {
Expand Down Expand Up @@ -44,9 +51,10 @@ describe('shiki syntax highlighting', () => {
const highlighter = await createShikiHighlighter();

const hast = await highlighter.codeToHast('const foo = "bar";', 'js');
const root = hast.children[0] as Element;

assert.match(hast.children[0].properties.class, /astro-code github-dark/);
assert.match(hast.children[0].properties.style, /background-color:#24292e;color:#e1e4e8;/);
assert.match(root.properties.class as string, /astro-code github-dark/);
assert.match(root.properties.style as string, /background-color:#24292e;color:#e1e4e8;/);
});

it('createShikiHighlighter can reuse the same instance for different languages', async () => {
Expand Down Expand Up @@ -76,11 +84,16 @@ describe('shiki syntax highlighting', () => {
'bicep',
'blade',
'bsl',
];
] as const;

const highlighters = new Set();
const highlighters = new Set<ShikiHighlighter>();
for (const lang of langs) {
highlighters.add(await createShikiHighlighter({ langs: [lang] }));
highlighters.add(
await createShikiHighlighter({
// @ts-expect-error: `langs` is typed as `LanguageRegistration[]`, but Shiki's `createHighlighter` accepts both strings and objects in the array.
langs: [lang],
}),
);
}

// Ensure that we only have one highlighter instance.
Expand Down Expand Up @@ -113,7 +126,11 @@ describe('shiki syntax highlighting', () => {
const highlighter = await createShikiHighlighter();

const html = await highlighter.codeToHtml(`foo`, 'js', {
attributes: { 'data-foo': 'bar', autofocus: true },
attributes: {
'data-foo': 'bar',
// @ts-expect-error: Shiki's `codeToHtml` accepts boolean attributes as `string | boolean`, but the types are currently incorrect.
autofocus: true,
},
});

assert.match(html, /data-foo="bar"/);
Expand Down Expand Up @@ -164,7 +181,7 @@ describe('shiki syntax highlighting', () => {
});

const html = await highlighter.codeToHtml(`let test = "some string"`, 'cjs', {
attributes: { 'data-foo': 'bar', autofocus: true },
attributes: { 'data-foo': 'bar' },
});

assert.match(html, /data-language="cjs"/);
Expand All @@ -188,12 +205,15 @@ describe('shiki syntax highlighting', () => {
clearShikiHighlighterCache();

const theme = 'github-light';
const highlighter = await createShikiHighlighter({ theme });
interface ShikiHighlighterInternal extends ShikiHighlighter {
loadLanguage(...langs: unknown[]): Promise<void>;
getLoadedLanguages(): string[];
}
const highlighter = (await createShikiHighlighter({ theme })) as ShikiHighlighterInternal;

// loadLanguage is an internal method
const loadLanguageArgs = [];
const originalLoadLanguage = highlighter['loadLanguage'];
highlighter['loadLanguage'] = async (...args) => {
const loadLanguageArgs: unknown[] = [];
const originalLoadLanguage = highlighter.loadLanguage;
highlighter.loadLanguage = async (...args: unknown[]) => {
loadLanguageArgs.push(...args);
return await originalLoadLanguage(...args);
};
Expand All @@ -202,19 +222,35 @@ describe('shiki syntax highlighting', () => {
assert.equal(loadLanguageArgs.length, 0);

// Load a new language
const h1 = await createShikiHighlighter({ theme, langs: ['js'] });
const h1 = await createShikiHighlighter({
theme,
// @ts-expect-error: `langs` is typed as `LanguageRegistration[]`, but Shiki's `createHighlighter` accepts both strings and objects in the array.
langs: ['js'],
});
assert.equal(loadLanguageArgs.length, 1);

// Load the same language again
const h2 = await createShikiHighlighter({ theme, langs: ['js'] });
const h2 = await createShikiHighlighter({
theme,
// @ts-expect-error: `langs` is typed as `LanguageRegistration[]`, but Shiki's `createHighlighter` accepts both strings and objects in the array.
langs: ['js'],
});
assert.equal(loadLanguageArgs.length, 1);

// Load another language
const h3 = await createShikiHighlighter({ theme, langs: ['ts'] });
const h3 = await createShikiHighlighter({
theme,
// @ts-expect-error: `langs` is typed as `LanguageRegistration[]`, but Shiki's `createHighlighter` accepts both strings and objects in the array.
langs: ['ts'],
});
assert.equal(loadLanguageArgs.length, 2);

// Load the same language again
const h4 = await createShikiHighlighter({ theme, langs: ['ts'] });
const h4 = await createShikiHighlighter({
theme,
// @ts-expect-error: `langs` is typed as `LanguageRegistration[]`, but Shiki's `createHighlighter` accepts both strings and objects in the array.
langs: ['ts'],
});
assert.equal(loadLanguageArgs.length, 2);

// All highlighters should be the same instance
Expand Down Expand Up @@ -251,7 +287,7 @@ describe('shiki syntax highlighting', () => {
it('uses a custom (ThemeRegistrationRaw) theme', async () => {
// Minimal subset of a custom theme — only the fields Shiki needs to
// derive the pre element's background-color and color.
const serendipityMorning = {
const serendipityMorning: ThemeRegistration = {
name: 'Serendipity Morning',
type: 'light',
colors: {
Expand Down Expand Up @@ -279,7 +315,7 @@ describe('shiki syntax highlighting', () => {
// Minimal rinfo grammar — same language used in the langs fixture.
// Must be passed as a LanguageRegistration (name + scopeName at top level),
// not the { id, grammar } wrapper used by Astro's config layer.
const riLang = {
const riLang: LanguageRegistration = {
name: 'rinfo',
scopeName: 'source.rinfo',
patterns: [{ include: '#lf-rinfo' }],
Expand Down
12 changes: 12 additions & 0 deletions packages/markdown/remark/tsconfig.test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": "../../../tsconfig.base.json",
"include": ["test/**/*.ts"],
"exclude": ["test/fixtures/**"],
"compilerOptions": {
"allowJs": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"rewriteRelativeImportExtensions": true
},
"references": [{ "path": "../../astro/tsconfig.test.json" }]
}
Loading