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
4 changes: 4 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ jobs:
- name: Run tests
run: pnpm run test:built

- name: Run source coverage
if: matrix.node-version == '22'
run: pnpm run test:coverage

# Reuses this job's checkout, dependencies, and build. The smoke query
# is milliseconds with the binary lookup but catches a linear regression.
- name: Run source-map benchmark smoke
Expand Down
116 changes: 116 additions & 0 deletions __tests__/unit/source-map.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import {
parseMd,
parseMdWithSourceMap,
SourceMapConsistencyError,
SourceMapUnavailableError,
} from '../../src/index';
import {
parseMdx,
parseMdxWithSourceMap,
} from '../../src/mdx';

function firstNode(root: any, type: string): any {
const queue = [root];
while (queue.length > 0) {
const node = queue.shift();
if (node.type === type) return node;
queue.push(...(node.children || []));
}
throw new Error(`No ${type} node found`);
}

function rawRange(source: string, range: any): string {
return source.slice(range.start.offset, range.end.offset);
}

describe('TypeScript source entry points', () => {
test('parses Markdown', () => {
const ast = parseMd('# Heading');
expect(ast.type).toBe('root');
expect(ast.children[0].type).toBe('heading');
});

test('maps a plain text value', () => {
const source = 'Hello world';
const { ast, sourceMap } = parseMdWithSourceMap(source);
const text = firstNode(ast, 'text');
expect(text.value).toBe(source);
expect(sourceMap.getRaw(text)).toBe(source);
expect(rawRange(source, sourceMap.getSourceRange(text, 6, 11)))
.toBe('world');
});

test('maps a character reference to its full source range', () => {
const source = 'A & B';
const { ast, sourceMap } = parseMdWithSourceMap(source);
const text = firstNode(ast, 'text');
expect(text.value).toBe('A & B');
expect(sourceMap.getRaw(text)).toBe(source);
expect(rawRange(source, sourceMap.getSourceRange(text, 2, 3)))
.toBe('&');
});

test('maps a link URL field', () => {
const source = '[link](a&b)';
const { ast, sourceMap } = parseMdWithSourceMap(source);
const link = firstNode(ast, 'link');
expect(link.url).toBe('a&b');
expect(rawRange(source, sourceMap.getFieldSourceRange(link, 'url', 1, 2)))
.toBe('&');
});

test('maps an inline code value', () => {
const source = '` a b `';
const { ast, sourceMap } = parseMdWithSourceMap(source);
const code = firstNode(ast, 'inlineCode');
expect(code.value).toBe('a b');
expect(rawRange(source, sourceMap.getSourceRange(code, 0, code.value.length)))
.toBe('a b');
});

test('reports a fenced code opening and insert point', () => {
const source = '```\ncode\n```';
const { ast, sourceMap } = parseMdWithSourceMap(source);
const code = firstNode(ast, 'code');
const info = sourceMap.getCodeSourceInfo(code);
expect(info.kind).toBe('fenced');
if (info.kind !== 'fenced') throw new Error('Expected fenced code');
expect(rawRange(source, info.openingFence)).toBe('```');
expect(info.infoInsertPoint.offset).toBe(3);
});

test('reports an indented code block', () => {
const { ast, sourceMap } = parseMdWithSourceMap(' code\n');
const code = firstNode(ast, 'code');
expect(sourceMap.getCodeSourceInfo(code)).toEqual({ kind: 'indented' });
});

test('rejects a text node modified after parsing', () => {
const { ast, sourceMap } = parseMdWithSourceMap('original');
const text = firstNode(ast, 'text');
text.value = 'changed';
expect(() => sourceMap.getSourceRange(text, 0, 1))
.toThrow(SourceMapConsistencyError);
});

test('rejects a node from another document', () => {
const first = parseMdWithSourceMap('first');
const second = parseMdWithSourceMap('second');
const foreignText = firstNode(second.ast, 'text');
expect(() => first.sourceMap.getRaw(foreignText))
.toThrow(SourceMapUnavailableError);
});

test('parses MDX and maps Markdown text within it', () => {
const source = '<Card />\n\nA &amp; B';
const ast = parseMdx(source);
expect(firstNode(ast, 'mdxJsxFlowElement').name).toBe('Card');

const mapped = parseMdxWithSourceMap(source);
const text = firstNode(mapped.ast, 'text');
expect(text.value).toBe('A & B');
expect(mapped.sourceMap.getRaw(text)).toBe('A &amp; B');
expect(rawRange(source, mapped.sourceMap.getSourceRange(text, 2, 3)))
.toBe('&amp;');
});
});
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module.exports = {
preset: 'ts-jest',
collectCoverage: false,
testRegex: '(./__tests__/.*\\.(test|spec))\\.ts$',
testPathIgnorePatterns: ['/node_modules/', '<rootDir>/__tests__/unit/'],
collectCoverageFrom: ['src/**/*.ts'],
coverageDirectory: '<rootDir>/coverage/',
testEnvironment: 'node',
Expand Down
29 changes: 29 additions & 0 deletions jest.source.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module.exports = {
testEnvironment: 'node',
testMatch: [
'<rootDir>/__tests__/unit/**/*.spec.ts',
],
extensionsToTreatAsEsm: ['.ts'],
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
useESM: true,
tsconfig: '<rootDir>/tsconfig.source-test.json',
},
],
},
collectCoverageFrom: [
'<rootDir>/src/**/*.ts',
'!<rootDir>/src/**/*.d.ts',
'!<rootDir>/src/types.ts',
'!<rootDir>/src/source-map/types.ts',
],
coverageProvider: 'v8',
coverageDirectory: '<rootDir>/coverage/source',
coverageReporters: [
'text',
'lcov',
'json-summary',
],
};
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@
"build:ts-declaration": "tsc && api-extractor run --local --verbose && api-extractor run --local --verbose --config api-extractor.mdx.json && cp dist/lint-md-parser.d.ts dist/lint-md-parser.d.cts && cp dist/lint-md-parser.d.ts dist/lint-md-parser.d.mts && cp dist/lint-md-parser-mdx.d.ts dist/lint-md-parser-mdx.d.cts && cp dist/lint-md-parser-mdx.d.ts dist/lint-md-parser-mdx.d.mts && for f in dist/lint-md-parser.d.ts dist/lint-md-parser.d.cts dist/lint-md-parser.d.mts dist/lint-md-parser-mdx.d.ts dist/lint-md-parser-mdx.d.cts dist/lint-md-parser-mdx.d.mts; do cat scripts/mdast-augment.d.ts >> \"$f\"; done",
"lint": "eslint --ext js,jsx,ts,tsx ./src",
"test": "pnpm run build && pnpm run test:built",
"test:built": "jest && pnpm run test:types && pnpm run test:package && pnpm run test:mdx3 && pnpm run test:bench-entry",
"test:built": "jest && pnpm run test:source && pnpm run test:types && pnpm run test:package && pnpm run test:mdx3 && pnpm run test:bench-entry",
"test:source": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --config jest.source.config.js",
"test:coverage": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --config jest.source.config.js --coverage",
"test:bench-entry": "node scripts/test-bench-markdown-versions.mjs",
"test:mdx3": "node scripts/test-mdx3.mjs",
"test:types": "tsc -p __tests__/types/tsconfig.json",
Expand Down
18 changes: 18 additions & 0 deletions tsconfig.source-test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": ".",
"outDir": ".tmp/jest-source",
"module": "ESNext",
"declaration": false
},
"include": [
"src/**/*.ts",
"__tests__/unit/**/*.ts"
],
"exclude": [
"node_modules",
"dist",
"coverage"
]
}
Loading