@abraca/convert 2.10.0 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -60,6 +60,9 @@ interface DocPageMeta extends Record<string, unknown> {
60
60
  spModelDocId?: string;
61
61
  slidesTransition?: 'none' | 'fade' | 'slide';
62
62
  slidesTheme?: 'dark' | 'light';
63
+ language?: string;
64
+ fileExtension?: string;
65
+ codeTheme?: string;
63
66
  __schemaVersion?: number;
64
67
  }
65
68
  //#endregion
@@ -118,6 +121,49 @@ declare function populateYDocFromHtml(fragment: Y.XmlFragment, html: string, fal
118
121
  */
119
122
  declare function appendHtmlToFragment(fragment: Y.XmlFragment, html: string): void;
120
123
  //#endregion
124
+ //#region packages/convert/src/code.d.ts
125
+ /** Y.Text key holding a code document's content. */
126
+ declare const CODE_TEXT_KEY = "code";
127
+ /** Read a code document's raw text. Empty string when absent. */
128
+ declare function readCodeText(doc: Y.Doc): string;
129
+ /**
130
+ * Replace a code document's full text in a single transaction. `origin`
131
+ * is forwarded to `transact` so callers (e.g. fs-sync) can tag the write
132
+ * and skip their own observers.
133
+ */
134
+ declare function writeCodeText(doc: Y.Doc, content: string, origin?: unknown): void;
135
+ /** Normalize an extension: strip leading dot(s), lowercase. */
136
+ declare function normalizeExtension(ext: string): string;
137
+ /**
138
+ * Whether a file extension should import as a `code` page type. Note
139
+ * `md`/`markdown` are intentionally NOT code extensions here — fs-sync
140
+ * treats `.md` as the prose/envelope format, so it's excluded from the
141
+ * code-import gate even though it has a language mapping for in-editor use.
142
+ */
143
+ declare function isCodeExtension(ext: string): boolean;
144
+ /** CodeMirror language id for an extension, or undefined when unknown. */
145
+ declare function extToLanguage(ext: string): string | undefined;
146
+ /** Preferred on-disk extension (no dot) for a language, or undefined. */
147
+ declare function languageToExtension(language: string): string | undefined;
148
+ /**
149
+ * Derive `{ language, fileExtension }` from a filename for code import.
150
+ * Returns undefined when the extension is not a recognized code extension
151
+ * (so callers fall back to the prose/doc path).
152
+ */
153
+ declare function inferCodeMetaFromFilename(filename: string): {
154
+ language: string;
155
+ fileExtension: string;
156
+ } | undefined;
157
+ /**
158
+ * Choose the on-disk extension for a code doc on export. Prefers an
159
+ * explicit `fileExtension`, then the language's primary extension, then
160
+ * `txt`.
161
+ */
162
+ declare function codeFileExtension(meta?: {
163
+ fileExtension?: string;
164
+ language?: string;
165
+ }): string;
166
+ //#endregion
121
167
  //#region packages/convert/src/diff.d.ts
122
168
  type JsonAttr = string | number | boolean | null | JsonAttr[] | {
123
169
  [key: string]: JsonAttr;
@@ -277,6 +323,8 @@ interface ManifestEntry {
277
323
  contentHash: string;
278
324
  lastWrittenAt: number;
279
325
  lastReadAt: number;
326
+ codePath?: string;
327
+ codeHash?: string;
280
328
  uploads: UploadManifestEntry[];
281
329
  }
282
330
  interface FsSyncManifest {
@@ -329,6 +377,13 @@ declare function hasChildren(docId: string, treeData: Record<string, FsTreeEntry
329
377
  * - Collision resolution via ~XXXX suffix
330
378
  */
331
379
  declare function buildRelativePath(docId: string, treeData: Record<string, FsTreeEntry>, manifest: FsSyncManifest): string;
380
+ /**
381
+ * Companion code-file path for a `code` doc, derived from its `.md`
382
+ * envelope path by swapping the extension. e.g.
383
+ * `src/main.md` + `rs` -> `src/main.rs`. A code doc never has children,
384
+ * so the `_index.md` form is not expected here, but is handled defensively.
385
+ */
386
+ declare function codeCompanionPath(mdRelativePath: string, fileExtension: string): string;
332
387
  /**
333
388
  * Get the directory portion of a relative path for a doc.
334
389
  * For _index.md docs: returns the directory containing _index.md
@@ -353,4 +408,4 @@ declare function getTreeData(treeMap: any): Record<string, FsTreeEntry>;
353
408
  */
354
409
  declare function nextOrder(treeData: Record<string, FsTreeEntry>, parentId: string | null): number;
355
410
  //#endregion
356
- export { type DocPageMeta, type FrontmatterResult, type FsAdapter, type FsSyncManifest, type FsTreeEntry, type JsonAttr, MARK_SPECS, MARK_SPEC_BY_DELIM, MARK_SPEC_BY_NAME, type ManifestEntry, type MarkAttrSpec, type MarkSpec, type MarkWireKind, type MetaValueType, NODE_SPECS, NODE_SPEC_BY_NAME, type NodeAttrSpec, type NodeSpec, type NodeWireKind, UNIVERSAL_META_KEYS, UNIVERSAL_META_KEY_NAMES, type UniversalMetaKey, type UploadManifestEntry, type YjsDiff, type YjsJsonElement, type YjsJsonNode, type YjsJsonText, type YjsTextRun, appendHtmlToFragment, buildAliasMap, buildRelativePath, buildReverseLookup, conflictsDir, createEmptyManifest, diffJson, diffYjs, filenameToLabel, fsFilenameToLabel, getDocDir, getFsAdapter, getTreeData, hasChildren, jsonToYFragment, labelToFilename, loadManifest, lookupByDocId, lookupByHash, lookupByPath, manifestDir, mdcTagOf, nextOrder, orphansDir, parseFrontmatter, populateYDocFromHtml, populateYDocFromMarkdown, removeEntry, resolveParentFromPath, saveManifest, setEntry, setFsAdapter, simpleHash, stringifyJsonNode, trashDir, yfragmentToJson, yjsToHtml, yjsToMarkdown, yjsToPlainText };
411
+ export { CODE_TEXT_KEY, type DocPageMeta, type FrontmatterResult, type FsAdapter, type FsSyncManifest, type FsTreeEntry, type JsonAttr, MARK_SPECS, MARK_SPEC_BY_DELIM, MARK_SPEC_BY_NAME, type ManifestEntry, type MarkAttrSpec, type MarkSpec, type MarkWireKind, type MetaValueType, NODE_SPECS, NODE_SPEC_BY_NAME, type NodeAttrSpec, type NodeSpec, type NodeWireKind, UNIVERSAL_META_KEYS, UNIVERSAL_META_KEY_NAMES, type UniversalMetaKey, type UploadManifestEntry, type YjsDiff, type YjsJsonElement, type YjsJsonNode, type YjsJsonText, type YjsTextRun, appendHtmlToFragment, buildAliasMap, buildRelativePath, buildReverseLookup, codeCompanionPath, codeFileExtension, conflictsDir, createEmptyManifest, diffJson, diffYjs, extToLanguage, filenameToLabel, fsFilenameToLabel, getDocDir, getFsAdapter, getTreeData, hasChildren, inferCodeMetaFromFilename, isCodeExtension, jsonToYFragment, labelToFilename, languageToExtension, loadManifest, lookupByDocId, lookupByHash, lookupByPath, manifestDir, mdcTagOf, nextOrder, normalizeExtension, orphansDir, parseFrontmatter, populateYDocFromHtml, populateYDocFromMarkdown, readCodeText, removeEntry, resolveParentFromPath, saveManifest, setEntry, setFsAdapter, simpleHash, stringifyJsonNode, trashDir, writeCodeText, yfragmentToJson, yjsToHtml, yjsToMarkdown, yjsToPlainText };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abraca/convert",
3
- "version": "2.10.0",
3
+ "version": "2.13.0",
4
4
  "description": "Bullet-proof Markdown ↔ Yjs/TipTap round-trip — canonical converter shared by cou-sh, abracadabra-nuxt, and @abraca/mcp.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/code.ts ADDED
@@ -0,0 +1,208 @@
1
+ // Code page-type helpers for @abraca/convert.
2
+ //
3
+ // A `code` document stores its content in a plain `Y.Text` under the
4
+ // `code` key (edited via CodeMirror + y-codemirror.next), separate from
5
+ // the TipTap prose fragment that carries the `documentHeader`/
6
+ // `documentMeta` envelope. fs-sync writes that text out to a real code
7
+ // file (e.g. `main.rs`) alongside the `.md` envelope.
8
+ //
9
+ // This module owns the single source of truth for:
10
+ // - the `code` Y.Text key name,
11
+ // - reading/replacing that text,
12
+ // - the file-extension ↔ CodeMirror-language mapping used by both
13
+ // fs-sync directions and the renderer.
14
+ //
15
+ // `yjs` stays a type-only peer import — these helpers only call methods
16
+ // on the Y.Doc handed in by the caller, never construct Y types.
17
+
18
+ import type * as Y from 'yjs'
19
+
20
+ /** Y.Text key holding a code document's content. */
21
+ export const CODE_TEXT_KEY = 'code'
22
+
23
+ /** Read a code document's raw text. Empty string when absent. */
24
+ export function readCodeText(doc: Y.Doc): string {
25
+ return doc.getText(CODE_TEXT_KEY).toString()
26
+ }
27
+
28
+ /**
29
+ * Replace a code document's full text in a single transaction. `origin`
30
+ * is forwarded to `transact` so callers (e.g. fs-sync) can tag the write
31
+ * and skip their own observers.
32
+ */
33
+ export function writeCodeText(doc: Y.Doc, content: string, origin?: unknown): void {
34
+ const ytext = doc.getText(CODE_TEXT_KEY)
35
+ doc.transact(() => {
36
+ if (ytext.length > 0) ytext.delete(0, ytext.length)
37
+ if (content.length > 0) ytext.insert(0, content)
38
+ }, origin)
39
+ }
40
+
41
+ // ── Extension ↔ language ─────────────────────────────────────────────────────
42
+ //
43
+ // Keys are lowercase extensions without the leading dot; values are the
44
+ // canonical CodeMirror language id the renderer resolves against
45
+ // `@codemirror/language-data`. The first extension listed for a language
46
+ // in EXT_BY_LANGUAGE is the one fs-sync uses when writing the file.
47
+
48
+ const LANGUAGE_BY_EXT: Readonly<Record<string, string>> = {
49
+ // JS/TS family
50
+ js: 'javascript',
51
+ mjs: 'javascript',
52
+ cjs: 'javascript',
53
+ jsx: 'jsx',
54
+ ts: 'typescript',
55
+ mts: 'typescript',
56
+ cts: 'typescript',
57
+ tsx: 'tsx',
58
+ // Web
59
+ html: 'html',
60
+ htm: 'html',
61
+ css: 'css',
62
+ scss: 'scss',
63
+ less: 'less',
64
+ vue: 'vue',
65
+ svelte: 'svelte',
66
+ // Data / config
67
+ json: 'json',
68
+ jsonc: 'json',
69
+ json5: 'json',
70
+ yaml: 'yaml',
71
+ yml: 'yaml',
72
+ toml: 'toml',
73
+ xml: 'xml',
74
+ ini: 'properties',
75
+ env: 'properties',
76
+ // Systems
77
+ rs: 'rust',
78
+ go: 'go',
79
+ c: 'c',
80
+ h: 'c',
81
+ cpp: 'cpp',
82
+ cc: 'cpp',
83
+ cxx: 'cpp',
84
+ hpp: 'cpp',
85
+ cs: 'csharp',
86
+ java: 'java',
87
+ kt: 'kotlin',
88
+ kts: 'kotlin',
89
+ swift: 'swift',
90
+ m: 'objective-c',
91
+ mm: 'objective-c',
92
+ // Scripting
93
+ py: 'python',
94
+ pyi: 'python',
95
+ rb: 'ruby',
96
+ php: 'php',
97
+ pl: 'perl',
98
+ lua: 'lua',
99
+ r: 'r',
100
+ // Shell
101
+ sh: 'shell',
102
+ bash: 'shell',
103
+ zsh: 'shell',
104
+ fish: 'shell',
105
+ // Query / misc
106
+ sql: 'sql',
107
+ graphql: 'graphql',
108
+ gql: 'graphql',
109
+ dockerfile: 'dockerfile',
110
+ // Markup that is genuinely "code" in a vault context
111
+ md: 'markdown',
112
+ markdown: 'markdown',
113
+ }
114
+
115
+ /** Preferred on-disk extension per language (reverse of LANGUAGE_BY_EXT). */
116
+ const EXT_BY_LANGUAGE: Readonly<Record<string, string>> = {
117
+ javascript: 'js',
118
+ jsx: 'jsx',
119
+ typescript: 'ts',
120
+ tsx: 'tsx',
121
+ html: 'html',
122
+ css: 'css',
123
+ scss: 'scss',
124
+ less: 'less',
125
+ vue: 'vue',
126
+ svelte: 'svelte',
127
+ json: 'json',
128
+ yaml: 'yaml',
129
+ toml: 'toml',
130
+ xml: 'xml',
131
+ properties: 'ini',
132
+ rust: 'rs',
133
+ go: 'go',
134
+ c: 'c',
135
+ cpp: 'cpp',
136
+ csharp: 'cs',
137
+ java: 'java',
138
+ kotlin: 'kt',
139
+ swift: 'swift',
140
+ 'objective-c': 'm',
141
+ python: 'py',
142
+ ruby: 'rb',
143
+ php: 'php',
144
+ perl: 'pl',
145
+ lua: 'lua',
146
+ r: 'r',
147
+ shell: 'sh',
148
+ sql: 'sql',
149
+ graphql: 'graphql',
150
+ dockerfile: 'dockerfile',
151
+ markdown: 'md',
152
+ plain: 'txt',
153
+ }
154
+
155
+ /** Normalize an extension: strip leading dot(s), lowercase. */
156
+ export function normalizeExtension(ext: string): string {
157
+ return ext.replace(/^\.+/, '').toLowerCase()
158
+ }
159
+
160
+ /**
161
+ * Whether a file extension should import as a `code` page type. Note
162
+ * `md`/`markdown` are intentionally NOT code extensions here — fs-sync
163
+ * treats `.md` as the prose/envelope format, so it's excluded from the
164
+ * code-import gate even though it has a language mapping for in-editor use.
165
+ */
166
+ export function isCodeExtension(ext: string): boolean {
167
+ const e = normalizeExtension(ext)
168
+ if (e === 'md' || e === 'markdown') return false
169
+ return e in LANGUAGE_BY_EXT
170
+ }
171
+
172
+ /** CodeMirror language id for an extension, or undefined when unknown. */
173
+ export function extToLanguage(ext: string): string | undefined {
174
+ return LANGUAGE_BY_EXT[normalizeExtension(ext)]
175
+ }
176
+
177
+ /** Preferred on-disk extension (no dot) for a language, or undefined. */
178
+ export function languageToExtension(language: string): string | undefined {
179
+ return EXT_BY_LANGUAGE[language.toLowerCase()]
180
+ }
181
+
182
+ /**
183
+ * Derive `{ language, fileExtension }` from a filename for code import.
184
+ * Returns undefined when the extension is not a recognized code extension
185
+ * (so callers fall back to the prose/doc path).
186
+ */
187
+ export function inferCodeMetaFromFilename(
188
+ filename: string,
189
+ ): { language: string, fileExtension: string } | undefined {
190
+ const dot = filename.lastIndexOf('.')
191
+ if (dot < 0) return undefined
192
+ const ext = normalizeExtension(filename.slice(dot + 1))
193
+ if (!isCodeExtension(ext)) return undefined
194
+ const language = LANGUAGE_BY_EXT[ext]
195
+ if (!language) return undefined
196
+ return { language, fileExtension: ext }
197
+ }
198
+
199
+ /**
200
+ * Choose the on-disk extension for a code doc on export. Prefers an
201
+ * explicit `fileExtension`, then the language's primary extension, then
202
+ * `txt`.
203
+ */
204
+ export function codeFileExtension(meta?: { fileExtension?: string, language?: string }): string {
205
+ if (meta?.fileExtension) return normalizeExtension(meta.fileExtension)
206
+ if (meta?.language) return languageToExtension(meta.language) ?? 'txt'
207
+ return 'txt'
208
+ }
@@ -58,6 +58,11 @@ export interface ManifestEntry {
58
58
  contentHash: string // hash of last-written/read markdown
59
59
  lastWrittenAt: number
60
60
  lastReadAt: number
61
+ // Code page type: the companion code file written alongside the `.md`
62
+ // envelope (e.g. "src/main.rs"), and the hash of its last-synced text.
63
+ // Present only for `code` docs.
64
+ codePath?: string
65
+ codeHash?: string
61
66
  uploads: UploadManifestEntry[]
62
67
  }
63
68
 
@@ -126,6 +126,23 @@ export function buildRelativePath(
126
126
  return `${parentPath}${parentPath ? '/' : ''}${resolvedFilename}.md`
127
127
  }
128
128
 
129
+ /**
130
+ * Companion code-file path for a `code` doc, derived from its `.md`
131
+ * envelope path by swapping the extension. e.g.
132
+ * `src/main.md` + `rs` -> `src/main.rs`. A code doc never has children,
133
+ * so the `_index.md` form is not expected here, but is handled defensively.
134
+ */
135
+ export function codeCompanionPath(mdRelativePath: string, fileExtension: string): string {
136
+ const ext = fileExtension.replace(/^\.+/, '').toLowerCase() || 'txt'
137
+ if (mdRelativePath.endsWith('/_index.md')) {
138
+ return `${mdRelativePath.slice(0, -'/_index.md'.length)}/_index.${ext}`
139
+ }
140
+ if (mdRelativePath.endsWith('.md')) {
141
+ return `${mdRelativePath.slice(0, -'.md'.length)}.${ext}`
142
+ }
143
+ return `${mdRelativePath}.${ext}`
144
+ }
145
+
129
146
  /**
130
147
  * Get the directory portion of a relative path for a doc.
131
148
  * For _index.md docs: returns the directory containing _index.md
package/src/index.ts CHANGED
@@ -24,6 +24,20 @@ export {
24
24
 
25
25
  export type { DocPageMeta } from './types.ts'
26
26
 
27
+ // Code page-type helpers — Y.Text('code') accessors + the canonical
28
+ // extension ↔ CodeMirror-language mapping shared by fs-sync and renderers.
29
+ export {
30
+ CODE_TEXT_KEY,
31
+ readCodeText,
32
+ writeCodeText,
33
+ normalizeExtension,
34
+ isCodeExtension,
35
+ extToLanguage,
36
+ languageToExtension,
37
+ inferCodeMetaFromFilename,
38
+ codeFileExtension,
39
+ } from './code.ts'
40
+
27
41
  // Structural Y.XmlFragment ↔ JSON converter + diff. Used by the
28
42
  // integration test suite and available to consumers who want to
29
43
  // author golden fixtures or do their own assertions.
@@ -94,6 +108,7 @@ export {
94
108
  fsFilenameToLabel,
95
109
  hasChildren,
96
110
  buildRelativePath,
111
+ codeCompanionPath,
97
112
  getDocDir,
98
113
  resolveParentFromPath,
99
114
  simpleHash,
@@ -146,6 +146,14 @@ export function parseFrontmatter(markdown: string): FrontmatterResult {
146
146
  const url = getStr(['url'])
147
147
  if (url) meta.url = url
148
148
 
149
+ // Code page type meta.
150
+ const language = getStr(['language'])
151
+ if (language) meta.language = language
152
+ const fileExtension = getStr(['fileExtension'])
153
+ if (fileExtension) meta.fileExtension = fileExtension
154
+ const codeTheme = getStr(['codeTheme'])
155
+ if (codeTheme) meta.codeTheme = codeTheme
156
+
149
157
  const ratingRaw = getStr(['rating'])
150
158
  if (ratingRaw !== undefined) {
151
159
  const n = Number(ratingRaw)
package/src/types.ts CHANGED
@@ -84,6 +84,11 @@ export interface DocPageMeta extends Record<string, unknown> {
84
84
  slidesTransition?: 'none' | 'fade' | 'slide'
85
85
  slidesTheme?: 'dark' | 'light'
86
86
 
87
+ // Code page type
88
+ language?: string
89
+ fileExtension?: string
90
+ codeTheme?: string
91
+
87
92
  // Schema migration version
88
93
  __schemaVersion?: number
89
94
  }
@@ -503,6 +503,11 @@ function generateFrontmatter(label: string | undefined, meta?: DocPageMeta, type
503
503
  }
504
504
 
505
505
  if (meta.checked !== undefined) lines.push(`checked: ${meta.checked}`)
506
+ // Code page type: syntax-highlight language + on-disk extension. These
507
+ // round-trip the `.md` envelope so the companion code file can be re-derived.
508
+ if (meta.language) lines.push(`language: ${yamlScalar(meta.language)}`)
509
+ if (meta.fileExtension) lines.push(`fileExtension: ${yamlScalar(meta.fileExtension)}`)
510
+ if (meta.codeTheme) lines.push(`codeTheme: ${yamlScalar(meta.codeTheme)}`)
506
511
  if (meta.dateStart) lines.push(`dateStart: "${escapeYaml(meta.dateStart)}"`)
507
512
  if (meta.dateEnd) lines.push(`dateEnd: "${escapeYaml(meta.dateEnd)}"`)
508
513
  if (meta.subtitle) lines.push(`subtitle: "${escapeYaml(meta.subtitle)}"`)