From 109e601ede29015fcbcdd37805be55e74f764471 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Thu, 23 Oct 2025 19:35:00 -0500 Subject: [PATCH 1/2] fix metadata tracking bug --- crates/quarto-yaml/claude-notes/implementation-plan.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/quarto-yaml/claude-notes/implementation-plan.md b/crates/quarto-yaml/claude-notes/implementation-plan.md index cc06cc3..984ba83 100644 --- a/crates/quarto-yaml/claude-notes/implementation-plan.md +++ b/crates/quarto-yaml/claude-notes/implementation-plan.md @@ -2,7 +2,7 @@ ## Overview -This crate implements `YamlWithSourceInfo`, a data structure that wraps `yaml-rust2::Yaml` with source location tracking. +This crate implements `YamlWithSourceInfo`, a data structure that wraps `yaml-rust2::Yaml` with source location tracking. This uses the **owned data approach** as decided in the design discussion (see `/Users/cscheid/repos/github/cscheid/kyoto/claude-notes/session-logs/2025-10-13-yaml-lifetime-vs-owned-discussion.md`). ## Architecture Decision: Owned Data @@ -159,3 +159,9 @@ impl MarkedEventReceiver for YamlBuilder { 3. **Unified SourceInfo** - Replace with project-wide SourceInfo type 4. **YAML tags** - Support for !expr and custom tags 5. **Multi-document** - Support YAML streams + +## References + +- Design document: `/Users/cscheid/repos/github/cscheid/kyoto/claude-notes/yaml-with-source-info-design.md` +- Session log: `/Users/cscheid/repos/github/cscheid/kyoto/claude-notes/session-logs/2025-10-13-yaml-lifetime-vs-owned-discussion.md` +- rust-analyzer patterns: `/Users/cscheid/repos/github/cscheid/kyoto/claude-notes/rust-analyzer-owned-data-patterns.md` From 7df054df52749a975c93c8ba58eb98b93ff1c7c4 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Fri, 24 Oct 2025 07:50:40 -0500 Subject: [PATCH 2/2] rename ts package, rehome at quarto-markdown --- ts-packages/README.md | 22 ++ .../.gitignore | 0 .../README.md | 35 +-- .../package.json | 7 +- .../src/index.ts | 37 ++- .../src/meta-converter.ts | 14 +- .../src/source-map.ts | 19 +- .../src/types.ts | 22 +- .../test/basic.test.ts | 46 +++- .../test/fixtures/math-with-attr.json | 1 + .../test/fixtures/math-with-attr.qmd | 13 + .../test/meta-conversion.test.ts | 0 .../test/source-map.test.ts | 2 +- .../test/type-safety.test.ts | 2 +- .../tsconfig.json | 4 +- ts-packages/rust-qmd-json/SETUP-NOTES.md | 49 ---- ts-packages/rust-qmd-json/package-lock.json | 227 ------------------ 17 files changed, 148 insertions(+), 352 deletions(-) create mode 100644 ts-packages/README.md rename ts-packages/{rust-qmd-json => annotated-qmd}/.gitignore (100%) rename ts-packages/{rust-qmd-json => annotated-qmd}/README.md (65%) rename ts-packages/{rust-qmd-json => annotated-qmd}/package.json (82%) rename ts-packages/{rust-qmd-json => annotated-qmd}/src/index.ts (72%) rename ts-packages/{rust-qmd-json => annotated-qmd}/src/meta-converter.ts (94%) rename ts-packages/{rust-qmd-json => annotated-qmd}/src/source-map.ts (93%) rename ts-packages/{rust-qmd-json => annotated-qmd}/src/types.ts (69%) rename ts-packages/{rust-qmd-json => annotated-qmd}/test/basic.test.ts (55%) create mode 100644 ts-packages/annotated-qmd/test/fixtures/math-with-attr.json create mode 100644 ts-packages/annotated-qmd/test/fixtures/math-with-attr.qmd rename ts-packages/{rust-qmd-json => annotated-qmd}/test/meta-conversion.test.ts (100%) rename ts-packages/{rust-qmd-json => annotated-qmd}/test/source-map.test.ts (98%) rename ts-packages/{rust-qmd-json => annotated-qmd}/test/type-safety.test.ts (98%) rename ts-packages/{rust-qmd-json => annotated-qmd}/tsconfig.json (88%) delete mode 100644 ts-packages/rust-qmd-json/SETUP-NOTES.md delete mode 100644 ts-packages/rust-qmd-json/package-lock.json diff --git a/ts-packages/README.md b/ts-packages/README.md new file mode 100644 index 0000000..0281cad --- /dev/null +++ b/ts-packages/README.md @@ -0,0 +1,22 @@ +# TypeScript Packages + +This directory contains standalone TypeScript packages associated with the Kyoto Rust workspace. + +Following the convention used in Rust monorepos (similar to how `target/` contains build artifacts), +this `ts-packages/` directory contains TypeScript packages that complement the Rust crates. + +## Packages + +- **annotated-qmd** (`@quarto/annotated-qmd`): Converts quarto-markdown-pandoc JSON output + to AnnotatedParse structures compatible with quarto-cli's YAML validation infrastructure. + +## Development + +Each package is independent with its own `package.json` and can be developed/tested separately: + +```bash +cd ts-packages/annotated-qmd +npm install +npm test +npm run build +``` diff --git a/ts-packages/rust-qmd-json/.gitignore b/ts-packages/annotated-qmd/.gitignore similarity index 100% rename from ts-packages/rust-qmd-json/.gitignore rename to ts-packages/annotated-qmd/.gitignore diff --git a/ts-packages/rust-qmd-json/README.md b/ts-packages/annotated-qmd/README.md similarity index 65% rename from ts-packages/rust-qmd-json/README.md rename to ts-packages/annotated-qmd/README.md index 38b4d84..f4e9e46 100644 --- a/ts-packages/rust-qmd-json/README.md +++ b/ts-packages/annotated-qmd/README.md @@ -1,4 +1,4 @@ -# @quarto/rust-qmd-json +# @quarto/annotated-qmd Convert quarto-markdown-pandoc JSON output to AnnotatedParse structures with full source mapping. @@ -11,14 +11,14 @@ infrastructure. It preserves complete source location information through the co ## Installation ```bash -npm install @quarto/rust-qmd-json +npm install @quarto/annotated-qmd ``` ## Quick Start ```typescript -import { parseRustQmdMetadata } from '@quarto/rust-qmd-json'; -import type { RustQmdJson } from '@quarto/rust-qmd-json'; +import { parseRustQmdMetadata } from '@quarto/annotated-qmd'; +import type { RustQmdJson } from '@quarto/annotated-qmd'; // JSON from quarto-markdown-pandoc const json: RustQmdJson = { @@ -58,7 +58,7 @@ Main entry point for converting quarto-markdown-pandoc JSON to AnnotatedParse. **Example with error handling:** ```typescript -import { parseRustQmdMetadata } from '@quarto/rust-qmd-json'; +import { parseRustQmdMetadata } from '@quarto/annotated-qmd'; const errorHandler = (msg: string, id?: number) => { console.error(`SourceInfo error: ${msg}`, id); @@ -81,7 +81,7 @@ import type { SerializableSourceInfo, SourceContext, SourceInfoErrorHandler -} from '@quarto/rust-qmd-json'; +} from '@quarto/annotated-qmd'; ``` ### Advanced Usage @@ -89,7 +89,7 @@ import type { For more control, you can use the underlying classes directly: ```typescript -import { SourceInfoReconstructor, MetadataConverter } from '@quarto/rust-qmd-json'; +import { SourceInfoReconstructor, MetadataConverter } from '@quarto/annotated-qmd'; const reconstructor = new SourceInfoReconstructor( json.source_pool, @@ -115,24 +115,3 @@ npm test # Clean npm run clean ``` - -## Architecture - -The conversion happens in two phases: - -1. **SourceInfo Reconstruction**: Convert the pooled SourceInfo format from JSON into - MappedString objects that track source locations through transformation chains. - -2. **Metadata Conversion**: Recursively convert MetaValue variants into AnnotatedParse - structures with proper source tracking. MetaInlines/MetaBlocks are treated as leaf - nodes with the JSON array structure preserved in the result. - -## Design Decisions - -- **Direct JSON Value Mapping**: MetaInlines and MetaBlocks are preserved as JSON arrays - in the `result` field, avoiding any text reconstruction -- **Source Tracking**: Every value can be traced back to original file location via SourceInfo -- **Compatible Types**: Produces AnnotatedParse structures compatible with existing validation code - -See repository's `claude-notes/plans/2025-10-23-json-to-annotated-parse-conversion.md` for -detailed implementation plan. diff --git a/ts-packages/rust-qmd-json/package.json b/ts-packages/annotated-qmd/package.json similarity index 82% rename from ts-packages/rust-qmd-json/package.json rename to ts-packages/annotated-qmd/package.json index 176a08f..9d0ef34 100644 --- a/ts-packages/rust-qmd-json/package.json +++ b/ts-packages/annotated-qmd/package.json @@ -1,6 +1,6 @@ { - "name": "@quarto/rust-qmd-json", - "version": "0.1.0", + "name": "@quarto/annotated-qmd", + "version": "0.1.1", "description": "Convert quarto-markdown-pandoc JSON output to AnnotatedParse structures", "license": "MIT", "author": { @@ -12,8 +12,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/quarto-dev/quarto.git", - "directory": "ts-packages/rust-qmd-json" + "url": "git+https://github.com/quarto-dev/quarto-markdown.git" }, "type": "module", "main": "dist/index.js", diff --git a/ts-packages/rust-qmd-json/src/index.ts b/ts-packages/annotated-qmd/src/index.ts similarity index 72% rename from ts-packages/rust-qmd-json/src/index.ts rename to ts-packages/annotated-qmd/src/index.ts index bdb139a..31d32ad 100644 --- a/ts-packages/rust-qmd-json/src/index.ts +++ b/ts-packages/annotated-qmd/src/index.ts @@ -1,5 +1,5 @@ /** - * @quarto/rust-qmd-json + * @quarto/annotated-qmd * * Converts quarto-markdown-pandoc JSON output to AnnotatedParse structures * compatible with quarto-cli's YAML validation infrastructure. @@ -43,21 +43,22 @@ import type { SourceInfoErrorHandler } from './source-map.js'; * * @example * ```typescript - * import { parseRustQmdMetadata } from '@quarto/rust-qmd-json'; + * import { parseRustQmdMetadata } from '@quarto/annotated-qmd'; * * const json = { * meta: { * title: { t: 'MetaString', c: 'Hello', s: 0 } * }, * blocks: [], - * source_pool: [ - * { r: [11, 16], t: 0, d: 0 } - * ], - * source_context: { + * astContext: { + * sourceInfoPool: [ + * { r: [11, 16], t: 0, d: 0 } + * ], * files: [ - * { id: 0, path: 'test.qmd', content: '---\ntitle: Hello\n---' } + * { name: 'test.qmd', content: '---\ntitle: Hello\n---' } * ] - * } + * }, + * 'pandoc-api-version': [1, 23, 1] * }; * * const metadata = parseRustQmdMetadata(json); @@ -68,15 +69,27 @@ export function parseRustQmdMetadata( json: RustQmdJson, errorHandler?: SourceInfoErrorHandler ): AnnotatedParse { + // Normalize the JSON structure to internal format + const sourceContext = { + files: json.astContext.files.map((f, idx) => ({ + id: idx, + path: f.name, + content: f.content || '' + })) + }; + // 1. Create SourceInfoReconstructor with pool and context const sourceReconstructor = new SourceInfoReconstructor( - json.source_pool, - json.source_context, + json.astContext.sourceInfoPool, + sourceContext, errorHandler ); - // 2. Create MetadataConverter - const converter = new MetadataConverter(sourceReconstructor); + // 2. Create MetadataConverter with metaTopLevelKeySources + const converter = new MetadataConverter( + sourceReconstructor, + json.astContext.metaTopLevelKeySources + ); // 3. Convert metadata to AnnotatedParse return converter.convertMeta(json.meta); diff --git a/ts-packages/rust-qmd-json/src/meta-converter.ts b/ts-packages/annotated-qmd/src/meta-converter.ts similarity index 94% rename from ts-packages/rust-qmd-json/src/meta-converter.ts rename to ts-packages/annotated-qmd/src/meta-converter.ts index 292818c..8441218 100644 --- a/ts-packages/rust-qmd-json/src/meta-converter.ts +++ b/ts-packages/annotated-qmd/src/meta-converter.ts @@ -49,7 +49,10 @@ function isTaggedSpan(obj: unknown): obj is { * Converts metadata from quarto-markdown-pandoc JSON to AnnotatedParse */ export class MetadataConverter { - constructor(private sourceReconstructor: SourceInfoReconstructor) {} + constructor( + private sourceReconstructor: SourceInfoReconstructor, + private metaTopLevelKeySources?: Record + ) {} /** * Convert top-level metadata object to AnnotatedParse @@ -58,7 +61,8 @@ export class MetadataConverter { // Create a synthetic MetaMap for the top-level metadata const entries: MetaMapEntry[] = Object.entries(jsonMeta).map(([key, value]) => ({ key, - key_source: value.s, // Use value's source for key (not ideal, but metadata doesn't include key sources) + // Use metaTopLevelKeySources if available, otherwise fall back to value's source + key_source: this.metaTopLevelKeySources?.[key] ?? value.s, value })); @@ -83,8 +87,10 @@ export class MetadataConverter { for (const [key, value] of Object.entries(jsonMeta)) { // Create AnnotatedParse for key - const [keyStart, keyEnd] = this.sourceReconstructor.getOffsets(value.s); - const keySource = this.sourceReconstructor.toMappedString(value.s); + // Use metaTopLevelKeySources if available, otherwise fall back to value's source + const keySourceId = this.metaTopLevelKeySources?.[key] ?? value.s; + const [keyStart, keyEnd] = this.sourceReconstructor.getOffsets(keySourceId); + const keySource = this.sourceReconstructor.toMappedString(keySourceId); const keyAP: AnnotatedParse = { result: key, diff --git a/ts-packages/rust-qmd-json/src/source-map.ts b/ts-packages/annotated-qmd/src/source-map.ts similarity index 93% rename from ts-packages/rust-qmd-json/src/source-map.ts rename to ts-packages/annotated-qmd/src/source-map.ts index 7cb1eb3..f0a13c5 100644 --- a/ts-packages/rust-qmd-json/src/source-map.ts +++ b/ts-packages/annotated-qmd/src/source-map.ts @@ -18,13 +18,11 @@ export interface SerializableSourceInfo { /** * Type guard for Concat data structure + * Rust serializes Concat data as a plain array: [[source_info_id, offset, length], ...] */ -function isConcatData(data: unknown): data is { pieces: [number, number, number][] } { - return ( - typeof data === 'object' && - data !== null && - 'pieces' in data && - Array.isArray((data as { pieces: unknown }).pieces) +function isConcatData(data: unknown): data is [number, number, number][] { + return Array.isArray(data) && data.every( + item => Array.isArray(item) && item.length === 3 ); } @@ -181,16 +179,17 @@ export class SourceInfoReconstructor { /** * Handle Concat SourceInfo type (t=2) - * Data format: {pieces: [[source_info_id, offset, length], ...]} + * Data format: [[source_info_id, offset, length], ...] + * (Rust serializes as plain array, not object with pieces field) */ private handleConcat(id: number, info: SerializableSourceInfo): MappedString { // Runtime type check if (!isConcatData(info.d)) { - this.errorHandler(`Invalid Concat data format (expected {pieces: [...]}), got ${typeof info.d}`, id); + this.errorHandler(`Invalid Concat data format (expected array of [id, offset, length]), got ${typeof info.d}`, id); return asMappedString(''); } - const pieces = info.d.pieces; + const pieces = info.d; // Direct array access // Build MappedString array from pieces const mappedPieces: MappedString[] = []; @@ -272,7 +271,7 @@ export class SourceInfoReconstructor { this.errorHandler(`Invalid Concat data format`, id); resolved = { file_id: -1, range: info.r }; } else { - const pieces = info.d.pieces; + const pieces = info.d; // Direct array access if (pieces.length === 0) { this.errorHandler(`Empty Concat pieces`, id); resolved = { file_id: -1, range: info.r }; diff --git a/ts-packages/rust-qmd-json/src/types.ts b/ts-packages/annotated-qmd/src/types.ts similarity index 69% rename from ts-packages/rust-qmd-json/src/types.ts rename to ts-packages/annotated-qmd/src/types.ts index a1408f3..c97c9c1 100644 --- a/ts-packages/rust-qmd-json/src/types.ts +++ b/ts-packages/annotated-qmd/src/types.ts @@ -47,18 +47,26 @@ export interface MetaMapEntry { value: JsonMetaValue; } +/** + * File information from Rust JSON output + */ +export interface RustFileInfo { + name: string; // File path/name + line_breaks?: number[]; // Byte offsets of newlines + total_length?: number; // Total file length in bytes + content?: string; // File content (populated by consumer) +} + /** * Complete JSON output from quarto-markdown-pandoc */ export interface RustQmdJson { meta: Record; blocks: unknown[]; // Not used in metadata conversion - source_pool: SerializableSourceInfo[]; - source_context: { - files: Array<{ - id: number; - path: string; - content: string; - }>; + astContext: { + sourceInfoPool: SerializableSourceInfo[]; + files: RustFileInfo[]; + metaTopLevelKeySources?: Record; // Maps metadata keys to SourceInfo IDs }; + 'pandoc-api-version': [number, number, number]; } diff --git a/ts-packages/rust-qmd-json/test/basic.test.ts b/ts-packages/annotated-qmd/test/basic.test.ts similarity index 55% rename from ts-packages/rust-qmd-json/test/basic.test.ts rename to ts-packages/annotated-qmd/test/basic.test.ts index 542ab7f..1678a4f 100644 --- a/ts-packages/rust-qmd-json/test/basic.test.ts +++ b/ts-packages/annotated-qmd/test/basic.test.ts @@ -41,15 +41,16 @@ test('can convert complete JSON to AnnotatedParse', async () => { author: { t: 'MetaString', c: 'Alice', s: 1 } }, blocks: [], - source_pool: [ - { r: [11, 22], t: 0, d: 0 }, // "Hello World" - { r: [31, 36], t: 0, d: 0 } // "Alice" - ], - source_context: { + astContext: { + sourceInfoPool: [ + { r: [11, 22], t: 0, d: 0 }, // "Hello World" + { r: [31, 36], t: 0, d: 0 } // "Alice" + ], files: [ - { id: 0, path: 'test.qmd', content: '---\ntitle: Hello World\nauthor: Alice\n---' } + { name: 'test.qmd', content: '---\ntitle: Hello World\nauthor: Alice\n---' } ] - } + }, + 'pandoc-api-version': [1, 23, 1] }; const result = parseRustQmdMetadata(json); @@ -60,3 +61,34 @@ test('can convert complete JSON to AnnotatedParse', async () => { assert.strictEqual((result.result as any).author, 'Alice'); assert.strictEqual(result.components.length, 4); // title key, title value, author key, author value }); + +test('can parse math-with-attr.json', async () => { + const { parseRustQmdMetadata } = await import('../src/index.js'); + const fs = await import('fs/promises'); + const path = await import('path'); + const { fileURLToPath } = await import('url'); + + // Get the directory of this test file + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + + // Load JSON fixture from test/fixtures + const jsonPath = path.join(__dirname, 'fixtures', 'math-with-attr.json'); + const jsonText = await fs.readFile(jsonPath, 'utf-8'); + const json = JSON.parse(jsonText); + + // Read the QMD file content from test/fixtures + const qmdPath = path.join(__dirname, 'fixtures', 'math-with-attr.qmd'); + const qmdContent = await fs.readFile(qmdPath, 'utf-8'); + + // Populate file content (simulating what user would do) + for (const file of json.astContext.files) { + file.content = qmdContent; + } + + const result = parseRustQmdMetadata(json); + + // Basic validation that it didn't throw + assert.strictEqual(result.kind, 'mapping'); + assert.ok(result.result); + assert.ok((result.result as any).title); +}); diff --git a/ts-packages/annotated-qmd/test/fixtures/math-with-attr.json b/ts-packages/annotated-qmd/test/fixtures/math-with-attr.json new file mode 100644 index 0000000..1a98be1 --- /dev/null +++ b/ts-packages/annotated-qmd/test/fixtures/math-with-attr.json @@ -0,0 +1 @@ +{"astContext":{"files":[{"line_breaks":[3,35,39,40,94,95,124,125,128,177,195,196,256],"name":"math-with-attr.qmd","total_length":257}],"metaTopLevelKeySources":{"title":47},"sourceInfoPool":[{"d":0,"r":[0,4],"t":0},{"d":0,"r":[4,5],"t":0},{"d":0,"r":[5,9],"t":0},{"d":0,"r":[9,10],"t":0},{"d":0,"r":[12,22],"t":0},{"d":0,"r":[10,24],"t":0},{"d":0,"r":[0,257],"t":0},{"d":6,"r":[4,35],"t":1},{"d":7,"r":[7,31],"t":1},{"d":0,"r":[41,47],"t":0},{"d":0,"r":[47,48],"t":0},{"d":0,"r":[48,52],"t":0},{"d":0,"r":[52,53],"t":0},{"d":0,"r":[53,57],"t":0},{"d":0,"r":[57,58],"t":0},{"d":0,"r":[58,67],"t":0},{"d":0,"r":[67,68],"t":0},{"d":[[15,0,9],[16,9,1]],"r":[0,10],"t":2},{"d":0,"r":[68,69],"t":0},{"d":0,"r":[69,79],"t":0},{"d":0,"r":[0,0],"t":0},{"d":0,"r":[41,95],"t":0},{"d":0,"r":[96,103],"t":0},{"d":0,"r":[103,104],"t":0},{"d":0,"r":[104,108],"t":0},{"d":0,"r":[108,109],"t":0},{"d":0,"r":[109,113],"t":0},{"d":0,"r":[113,114],"t":0},{"d":0,"r":[114,123],"t":0},{"d":0,"r":[123,124],"t":0},{"d":[[28,0,9],[29,9,1]],"r":[0,10],"t":2},{"d":0,"r":[96,125],"t":0},{"d":0,"r":[126,180],"t":0},{"d":0,"r":[0,0],"t":0},{"d":0,"r":[126,196],"t":0},{"d":0,"r":[197,204],"t":0},{"d":0,"r":[204,205],"t":0},{"d":0,"r":[205,211],"t":0},{"d":0,"r":[211,212],"t":0},{"d":0,"r":[212,219],"t":0},{"d":0,"r":[219,220],"t":0},{"d":[[39,0,7],[40,7,1]],"r":[0,8],"t":2},{"d":0,"r":[220,221],"t":0},{"d":0,"r":[221,238],"t":0},{"d":0,"r":[0,0],"t":0},{"d":0,"r":[197,257],"t":0},{"d":6,"r":[4,35],"t":1},{"d":46,"r":[0,5],"t":1}]},"blocks":[{"c":[{"c":"Inline","s":9,"t":"Str"},{"s":10,"t":"Space"},{"c":"math","s":11,"t":"Str"},{"s":12,"t":"Space"},{"c":"with","s":13,"t":"Str"},{"s":14,"t":"Space"},{"c":"attribute:","s":17,"t":"Str"},{"s":18,"t":"Space"},{"c":[["eq-einstein",["quarto-math-with-attribute"],[]],[{"c":[{"t":"InlineMath"},"E = mc^2"],"s":19,"t":"Math"}]],"s":20,"t":"Span"}],"s":21,"t":"Para"},{"c":[{"c":"Display","s":22,"t":"Str"},{"s":23,"t":"Space"},{"c":"math","s":24,"t":"Str"},{"s":25,"t":"Space"},{"c":"with","s":26,"t":"Str"},{"s":27,"t":"Space"},{"c":"attribute:","s":30,"t":"Str"}],"s":31,"t":"Para"},{"c":[{"c":[["eq-gaussian",["quarto-math-with-attribute"],[]],[{"c":[{"t":"DisplayMath"},"\n\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}\n"],"s":32,"t":"Math"}]],"s":33,"t":"Span"}],"s":34,"t":"Para"},{"c":[{"c":"Another","s":35,"t":"Str"},{"s":36,"t":"Space"},{"c":"inline","s":37,"t":"Str"},{"s":38,"t":"Space"},{"c":"example:","s":41,"t":"Str"},{"s":42,"t":"Space"},{"c":[["eq-pythagorean",["quarto-math-with-attribute"],[]],[{"c":[{"t":"InlineMath"},"a^2 + b^2 = c^2"],"s":43,"t":"Math"}]],"s":44,"t":"Span"}],"s":45,"t":"Para"}],"meta":{"title":{"c":[{"c":"math","s":0,"t":"Str"},{"s":1,"t":"Space"},{"c":"with","s":2,"t":"Str"},{"s":3,"t":"Space"},{"c":[{"c":"attributes","s":4,"t":"Str"}],"s":5,"t":"Strong"}],"s":8,"t":"MetaInlines"}},"pandoc-api-version":[1,23,1]} \ No newline at end of file diff --git a/ts-packages/annotated-qmd/test/fixtures/math-with-attr.qmd b/ts-packages/annotated-qmd/test/fixtures/math-with-attr.qmd new file mode 100644 index 0000000..2069919 --- /dev/null +++ b/ts-packages/annotated-qmd/test/fixtures/math-with-attr.qmd @@ -0,0 +1,13 @@ +--- +title: math with **attributes** +--- + +Inline math with attribute: $E = mc^2$ {#eq-einstein} + +Display math with attribute: + +$$ +\int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2} +$$ {#eq-gaussian} + +Another inline example: $a^2 + b^2 = c^2$ {#eq-pythagorean} diff --git a/ts-packages/rust-qmd-json/test/meta-conversion.test.ts b/ts-packages/annotated-qmd/test/meta-conversion.test.ts similarity index 100% rename from ts-packages/rust-qmd-json/test/meta-conversion.test.ts rename to ts-packages/annotated-qmd/test/meta-conversion.test.ts diff --git a/ts-packages/rust-qmd-json/test/source-map.test.ts b/ts-packages/annotated-qmd/test/source-map.test.ts similarity index 98% rename from ts-packages/rust-qmd-json/test/source-map.test.ts rename to ts-packages/annotated-qmd/test/source-map.test.ts index 66de198..ce1abe5 100644 --- a/ts-packages/rust-qmd-json/test/source-map.test.ts +++ b/ts-packages/annotated-qmd/test/source-map.test.ts @@ -84,7 +84,7 @@ console.log('Running SourceInfo reconstruction tests...'); { r: [0, 10], t: 2, - d: { pieces: [[0, 0, 5], [1, 0, 5]] } // Concat "Hello" + "World" + d: [[0, 0, 5], [1, 0, 5]] // Concat "Hello" + "World" (array format from Rust) } ]; diff --git a/ts-packages/rust-qmd-json/test/type-safety.test.ts b/ts-packages/annotated-qmd/test/type-safety.test.ts similarity index 98% rename from ts-packages/rust-qmd-json/test/type-safety.test.ts rename to ts-packages/annotated-qmd/test/type-safety.test.ts index e5cb68d..4bee79b 100644 --- a/ts-packages/rust-qmd-json/test/type-safety.test.ts +++ b/ts-packages/annotated-qmd/test/type-safety.test.ts @@ -72,7 +72,7 @@ console.log('Running type safety tests...'); const pool: SerializableSourceInfo[] = [ { r: [0, 5], t: 0, d: 0 }, - { r: [0, 10], t: 2, d: 'not-an-object' as unknown } // Should be {pieces: [...]} + { r: [0, 10], t: 2, d: 'not-an-array' as unknown } // Should be [[id, offset, length], ...] ]; let errorCalled = false; diff --git a/ts-packages/rust-qmd-json/tsconfig.json b/ts-packages/annotated-qmd/tsconfig.json similarity index 88% rename from ts-packages/rust-qmd-json/tsconfig.json rename to ts-packages/annotated-qmd/tsconfig.json index 4553cf1..2571de6 100644 --- a/ts-packages/rust-qmd-json/tsconfig.json +++ b/ts-packages/annotated-qmd/tsconfig.json @@ -1,9 +1,9 @@ { "compilerOptions": { "target": "ES2022", - "module": "ES2022", + "module": "node16", "lib": ["ES2022"], - "moduleResolution": "node", + "moduleResolution": "node16", "outDir": "./dist", "rootDir": "./src", "strict": true, diff --git a/ts-packages/rust-qmd-json/SETUP-NOTES.md b/ts-packages/rust-qmd-json/SETUP-NOTES.md deleted file mode 100644 index 1317415..0000000 --- a/ts-packages/rust-qmd-json/SETUP-NOTES.md +++ /dev/null @@ -1,49 +0,0 @@ -# Setup Notes - -## TypeScript Infrastructure Verified - -✅ Successfully set up standalone TypeScript package -✅ `@quarto/mapped-string` integration working -✅ All tests passing - -## Test Results - -``` -✔ can import and use @quarto/mapped-string (0.550583ms) -✔ can create mapped substrings (0.106792ms) -✔ placeholder conversion function (15.146917ms) -ℹ tests 3 -ℹ suites 0 -ℹ pass 3 -ℹ fail 0 -``` - -## Environment - -- Node.js: v23.11.0 -- npm: 10.9.2 -- TypeScript: ^5.4.2 -- Dependencies: - - `@quarto/mapped-string`: ^0.1.8 (working correctly) - - `tsx`: ^4.7.1 (for running TypeScript tests) - - `@types/node`: ^20.0.0 - -## Key Learnings - -1. **Module imports**: @quarto/mapped-string exports `MappedString` as a type, not a value. - Must use `export type { MappedString }` in TypeScript. - -2. **Test framework**: Using Node.js built-in test runner with tsx for TypeScript execution. - Works well for ES modules. - -3. **Project structure**: Following Rust workspace conventions by placing TypeScript packages - in `ts-packages/` directory parallel to `crates/`. - -## Next Steps - -Ready to implement: -1. Phase 1: SourceInfo reconstruction -2. Phase 2: Metadata conversion -3. Phase 3: Integration & testing - -See `claude-notes/plans/2025-10-23-json-to-annotated-parse-conversion.md` for detailed plan. diff --git a/ts-packages/rust-qmd-json/package-lock.json b/ts-packages/rust-qmd-json/package-lock.json deleted file mode 100644 index 2456082..0000000 --- a/ts-packages/rust-qmd-json/package-lock.json +++ /dev/null @@ -1,227 +0,0 @@ -{ - "name": "@quarto/rust-qmd-json", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@quarto/rust-qmd-json", - "version": "0.1.0", - "license": "MIT", - "dependencies": { - "@quarto/mapped-string": "^0.1.8" - }, - "devDependencies": { - "@types/node": "^20.0.0", - "tsx": "^4.7.1", - "typescript": "^5.4.2" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", - "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@quarto/mapped-string": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@quarto/mapped-string/-/mapped-string-0.1.8.tgz", - "integrity": "sha512-NkHKvyola1Gw9RvI6JhOT6kvFx0HXgzXOay2LlF2gA09VkASCYaDaeWa5jME+c27tdBZ95IUueSAYFroJyrTJQ==", - "license": "MIT", - "dependencies": { - "@quarto/tidyverse-errors": "^0.1.9", - "ansi-colors": "^4.1.3", - "tsconfig": "*", - "typescript": "^5.4.2" - } - }, - "node_modules/@quarto/tidyverse-errors": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/@quarto/tidyverse-errors/-/tidyverse-errors-0.1.9.tgz", - "integrity": "sha512-JWA/teFA0XOv1UbAmNPX8bymBes/U0o9KNbvY0Aw1Mg7wY+vFRaVFWOicQuO6HrXtVM/6Osyy7IFY0KfKndy5w==", - "license": "MIT", - "dependencies": { - "ansi-colors": "^4.1.3", - "tsconfig": "*", - "typescript": "^5.4.2" - } - }, - "node_modules/@types/node": { - "version": "20.19.23", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.23.tgz", - "integrity": "sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", - "license": "MIT" - }, - "node_modules/@types/strip-json-comments": { - "version": "0.0.30", - "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", - "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", - "license": "MIT" - }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/esbuild": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", - "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.11", - "@esbuild/android-arm": "0.25.11", - "@esbuild/android-arm64": "0.25.11", - "@esbuild/android-x64": "0.25.11", - "@esbuild/darwin-arm64": "0.25.11", - "@esbuild/darwin-x64": "0.25.11", - "@esbuild/freebsd-arm64": "0.25.11", - "@esbuild/freebsd-x64": "0.25.11", - "@esbuild/linux-arm": "0.25.11", - "@esbuild/linux-arm64": "0.25.11", - "@esbuild/linux-ia32": "0.25.11", - "@esbuild/linux-loong64": "0.25.11", - "@esbuild/linux-mips64el": "0.25.11", - "@esbuild/linux-ppc64": "0.25.11", - "@esbuild/linux-riscv64": "0.25.11", - "@esbuild/linux-s390x": "0.25.11", - "@esbuild/linux-x64": "0.25.11", - "@esbuild/netbsd-arm64": "0.25.11", - "@esbuild/netbsd-x64": "0.25.11", - "@esbuild/openbsd-arm64": "0.25.11", - "@esbuild/openbsd-x64": "0.25.11", - "@esbuild/openharmony-arm64": "0.25.11", - "@esbuild/sunos-x64": "0.25.11", - "@esbuild/win32-arm64": "0.25.11", - "@esbuild/win32-ia32": "0.25.11", - "@esbuild/win32-x64": "0.25.11" - } - }, - "node_modules/get-tsconfig": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", - "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tsconfig": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", - "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", - "license": "MIT", - "dependencies": { - "@types/strip-bom": "^3.0.0", - "@types/strip-json-comments": "0.0.30", - "strip-bom": "^3.0.0", - "strip-json-comments": "^2.0.0" - } - }, - "node_modules/tsx": { - "version": "4.20.6", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", - "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.25.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - } - } -}