gt 2.17.1 → 2.17.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,58 @@
1
1
  # gtx-cli
2
2
 
3
+ ## 2.17.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [#2221](https://github.com/generaltranslation/gt/pull/2221) [`cadd04a`](https://github.com/generaltranslation/gt/commit/cadd04a07d73fd6090c3f72a199f03e443b5db10) Thanks [@fernando-aviles](https://github.com/fernando-aviles)! - Fix custom heading IDs (`## Heading {#id}`) being dropped or misapplied in translated MD/MDX.
8
+
9
+ `{#id}` is not valid MDX — remark-mdx hands it to acorn as an expression — so files
10
+ using Mintlify's custom heading ID syntax failed validation and were skipped
11
+ entirely. Parsing now tolerates the syntax, so `skipFileValidation` is no longer
12
+ needed to translate those files.
13
+
14
+ Anchor IDs are also applied far more reliably:
15
+ - Headings are located by parser line positions instead of by matching heading
16
+ text, so indentation (headings nested in `<Tabs>`, `<Steps>`, `<Accordion>`),
17
+ inline JSX, escaped characters and repeated heading text no longer cause a
18
+ heading to be skipped or an ID to land on the wrong heading.
19
+ - Repeated headings now get unique IDs (`slug`, `slug-2`, `slug-3`) matching how
20
+ Mintlify disambiguates them, instead of emitting the same ID several times.
21
+ - Source and translated files are now read with the same extractor. Previously a
22
+ source using `{#id}` fell back to line scanning while its translation used the
23
+ AST, so the two heading lists could disagree and shift every ID after the first
24
+ nested heading.
25
+ - In `experimentalAddHeaderAnchorIds: 'mintlify'` mode, an author-written `{#id}`
26
+ is carried into the translation in Mintlify's native inline syntax rather than
27
+ being replaced by a `<div id>` wrapper. Wrappers are still used for IDs the CLI
28
+ derives from heading text.
29
+ - Applying an anchor no longer re-stringifies the whole document, so it no longer
30
+ HTML-escapes unrelated heading text or reformats the file.
31
+ - An existing wrapper is recognized from the parsed tree rather than by matching
32
+ the tag's text, so extra attributes, single quotes, a multi-line tag or a
33
+ non-`div` element no longer cause a second wrapper to be nested inside the first.
34
+ - An inline anchor is inserted before a heading's closing `##` sequence instead of
35
+ after it, which previously turned the closing hashes into visible heading text.
36
+
37
+ Anchor processing now runs after the other MD/MDX post-processing passes, which
38
+ re-indent headings nested in JSX when they stringify.
39
+
40
+ - Updated dependencies []:
41
+ - generaltranslation@9.1.10
42
+ - @generaltranslation/python-extractor@0.2.43
43
+ - @generaltranslation/supported-locales@2.1.23
44
+ - @generaltranslation/vue-extractor@0.1.3
45
+
46
+ ## 2.17.2
47
+
48
+ ### Patch Changes
49
+
50
+ - Updated dependencies []:
51
+ - generaltranslation@9.1.9
52
+ - @generaltranslation/python-extractor@0.2.42
53
+ - @generaltranslation/supported-locales@2.1.22
54
+ - @generaltranslation/vue-extractor@0.1.2
55
+
3
56
  ## 2.17.1
4
57
 
5
58
  ### Patch Changes
@@ -49,8 +49,8 @@ async function postProcessTranslations(settings, includeFiles) {
49
49
  const nonDefaultLocales = settings.locales.filter((locale) => locale !== settings.defaultLocale);
50
50
  if (nonDefaultLocales.length > 0) await localizeRelativeAssets(settings, nonDefaultLocales, postProcessIncludes);
51
51
  }
52
- if (settings.options?.experimentalLocalizeStaticUrls || settings.options?.experimentalAddHeaderAnchorIds) await processAnchorIds(settings, postProcessIncludes);
53
52
  if (settings.options?.experimentalLocalizeStaticImports) await localizeStaticImports(settings, postProcessIncludes);
53
+ if (settings.options?.experimentalLocalizeStaticUrls || settings.options?.experimentalAddHeaderAnchorIds) await processAnchorIds(settings, postProcessIncludes);
54
54
  if (settings.options?.experimentalFlattenJsonFiles) await flattenJsonFiles(settings, postProcessIncludes);
55
55
  if (settings.options?.copyFiles) await copyFile(settings);
56
56
  persistPostProcessHashes(settings, postProcessIncludes, getDownloadedMeta());
@@ -1 +1 @@
1
- {"version":3,"file":"translate.js","names":[],"sources":["../../../src/cli/commands/translate.ts"],"sourcesContent":["import { EnqueueFilesResult } from 'generaltranslation/types';\nimport { TranslateFlags } from '../../types/index.js';\nimport { Settings } from '../../types/index.js';\nimport {\n FileTranslationData,\n runDownloadWorkflow,\n} from '../../workflows/download.js';\nimport { createFileMapping } from '../../formats/files/fileMapping.js';\nimport copyFile from '../../fs/copyFile.js';\nimport flattenJsonFiles from '../../utils/flattenJsonFiles.js';\nimport localizeStaticUrls from '../../utils/localizeStaticUrls.js';\nimport localizeRelativeAssets from '../../utils/localizeRelativeAssets.js';\nimport processAnchorIds from '../../utils/processAnchorIds.js';\nimport localizeStaticImports from '../../utils/localizeStaticImports.js';\nimport { postprocessMintlify } from '../../formats/files/postprocess/mintlify.js';\nimport { BranchData } from '../../types/branch.js';\nimport { getDownloadedMeta } from '../../state/recentDownloads.js';\nimport { persistPostProcessHashes } from '../../utils/persistPostprocessHashes.js';\nimport { runPublishWorkflow } from '../../workflows/publish.js';\nimport { SUPPORTED_FILE_EXTENSIONS } from '../../formats/files/supportedFiles.js';\nimport { hasNonIdentityFileFormatTransformForType } from '../../formats/files/transformFormat.js';\nimport { getRelative } from '../../fs/findFilepath.js';\nimport type { InlineLibrary } from '../../types/libraries.js';\n\n// Downloads translations that were completed\nexport async function handleTranslate(\n options: TranslateFlags,\n settings: Settings,\n fileVersionData: FileTranslationData | undefined,\n jobData: EnqueueFilesResult | undefined,\n branchData: BranchData | undefined,\n publishMap?: Map<string, boolean>,\n inlineLibrary?: InlineLibrary\n) {\n if (fileVersionData) {\n const {\n resolvedPaths,\n placeholderPaths,\n transformPaths,\n transformFormats,\n } = settings.files;\n\n const fileMapping = createFileMapping(\n resolvedPaths,\n placeholderPaths,\n transformPaths,\n transformFormats,\n settings.locales,\n settings.defaultLocale\n );\n // Check for remaining translations\n await runDownloadWorkflow({\n fileVersionData: fileVersionData,\n jobData: jobData,\n branchData: branchData,\n locales: settings.locales,\n timeoutDuration: options.timeout,\n resolveOutputPath: (sourcePath, locale) =>\n fileMapping[locale]?.[sourcePath] ?? null,\n options: settings,\n inlineLibrary,\n forceRetranslation: options.force,\n forceDownload: options.forceDownload || options.force, // if force is true should also force download\n });\n\n // Publish/unpublish files after translations are downloaded\n if (publishMap && branchData?.currentBranch.id) {\n const files = Object.entries(fileVersionData).map(([fileId, data]) => ({\n fileId,\n versionId: data.versionId,\n fileName: data.fileName,\n }));\n await runPublishWorkflow(\n files,\n publishMap,\n branchData.currentBranch.id,\n settings\n );\n }\n }\n}\n\nexport async function postProcessTranslations(\n settings: Settings,\n includeFiles?: Set<string>\n) {\n const postProcessIncludes = filterPostProcessIncludesForFormatTransforms(\n settings,\n includeFiles\n );\n if (includeFiles && postProcessIncludes?.size === 0) return;\n\n await postprocessMintlify(settings, postProcessIncludes);\n\n // Localize static urls (/docs -> /[locale]/docs) and preserve anchor IDs for non-default locales\n // Default locale is processed earlier in the flow in base.ts\n if (settings.options?.experimentalLocalizeStaticUrls) {\n const nonDefaultLocales = settings.locales.filter(\n (locale) => locale !== settings.defaultLocale\n );\n if (nonDefaultLocales.length > 0) {\n await localizeStaticUrls(\n settings,\n nonDefaultLocales,\n postProcessIncludes\n );\n }\n }\n\n // Rewrite relative asset URLs in translated md/mdx files\n if (settings.options?.experimentalLocalizeRelativeAssets) {\n const nonDefaultLocales = settings.locales.filter(\n (locale) => locale !== settings.defaultLocale\n );\n if (nonDefaultLocales.length > 0) {\n await localizeRelativeAssets(\n settings,\n nonDefaultLocales,\n postProcessIncludes\n );\n }\n }\n\n const shouldProcessAnchorIds =\n settings.options?.experimentalLocalizeStaticUrls ||\n settings.options?.experimentalAddHeaderAnchorIds;\n\n // Add explicit anchor IDs to translated MDX/MD files to preserve navigation\n // Uses inline {#id} format by default, or div wrapping if experimentalAddHeaderAnchorIds is 'mintlify'\n if (shouldProcessAnchorIds) {\n await processAnchorIds(settings, postProcessIncludes);\n }\n\n // Localize static imports (import Snippet from /snippets/file.mdx -> import Snippet from /snippets/[locale]/file.mdx)\n if (settings.options?.experimentalLocalizeStaticImports) {\n await localizeStaticImports(settings, postProcessIncludes);\n }\n\n // Flatten json files into a single file\n if (settings.options?.experimentalFlattenJsonFiles) {\n await flattenJsonFiles(settings, postProcessIncludes);\n }\n\n // Copy files to the target locale\n if (settings.options?.copyFiles) {\n await copyFile(settings);\n }\n\n // Record postprocessed content hashes for newly downloaded files\n persistPostProcessHashes(settings, postProcessIncludes, getDownloadedMeta());\n}\n\n/**\n * Exclude only outputs whose source file was translated into a different format.\n * @param settings - The settings for the project\n * @param includeFiles - The files to include in the post-processing\n * @returns The files to exclude in the post-processing\n */\nfunction filterPostProcessIncludesForFormatTransforms(\n settings: Settings,\n includeFiles?: Set<string>\n): Set<string> | undefined {\n if (!includeFiles) return includeFiles;\n\n const transformedSourcePaths = new Set<string>();\n for (const fileType of SUPPORTED_FILE_EXTENSIONS) {\n if (!hasNonIdentityFileFormatTransformForType(settings, fileType)) continue;\n\n for (const sourcePath of settings.files.resolvedPaths[fileType] || []) {\n transformedSourcePaths.add(getRelative(sourcePath));\n }\n }\n if (transformedSourcePaths.size === 0) return includeFiles;\n\n const { resolvedPaths, placeholderPaths, transformPaths, transformFormats } =\n settings.files;\n const fileMapping = createFileMapping(\n resolvedPaths,\n placeholderPaths,\n transformPaths,\n transformFormats,\n settings.locales,\n settings.defaultLocale\n );\n\n const transformedOutputPaths = new Set<string>();\n for (const localeMapping of Object.values(fileMapping)) {\n for (const [sourcePath, outputPath] of Object.entries(localeMapping)) {\n if (transformedSourcePaths.has(sourcePath)) {\n transformedOutputPaths.add(outputPath);\n }\n }\n }\n\n return new Set(\n [...includeFiles].filter(\n (filePath) => !transformedOutputPaths.has(filePath)\n )\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAyBA,eAAsB,gBACpB,SACA,UACA,iBACA,SACA,YACA,YACA,eACA;AACA,KAAI,iBAAiB;EACnB,MAAM,EACJ,eACA,kBACA,gBACA,qBACE,SAAS;EAEb,MAAM,cAAc,kBAClB,eACA,kBACA,gBACA,kBACA,SAAS,SACT,SAAS,cACV;AAED,QAAM,oBAAoB;GACP;GACR;GACG;GACZ,SAAS,SAAS;GAClB,iBAAiB,QAAQ;GACzB,oBAAoB,YAAY,WAC9B,YAAY,UAAU,eAAe;GACvC,SAAS;GACT;GACA,oBAAoB,QAAQ;GAC5B,eAAe,QAAQ,iBAAiB,QAAQ;GACjD,CAAC;AAGF,MAAI,cAAc,YAAY,cAAc,GAM1C,OAAM,mBALQ,OAAO,QAAQ,gBAAgB,CAAC,KAAK,CAAC,QAAQ,WAAW;GACrE;GACA,WAAW,KAAK;GAChB,UAAU,KAAK;GAChB,EAEM,EACL,YACA,WAAW,cAAc,IACzB,SACD;;;AAKP,eAAsB,wBACpB,UACA,cACA;CACA,MAAM,sBAAsB,6CAC1B,UACA,aACD;AACD,KAAI,gBAAgB,qBAAqB,SAAS,EAAG;AAErD,OAAM,oBAAoB,UAAU,oBAAoB;AAIxD,KAAI,SAAS,SAAS,gCAAgC;EACpD,MAAM,oBAAoB,SAAS,QAAQ,QACxC,WAAW,WAAW,SAAS,cACjC;AACD,MAAI,kBAAkB,SAAS,EAC7B,OAAM,mBACJ,UACA,mBACA,oBACD;;AAKL,KAAI,SAAS,SAAS,oCAAoC;EACxD,MAAM,oBAAoB,SAAS,QAAQ,QACxC,WAAW,WAAW,SAAS,cACjC;AACD,MAAI,kBAAkB,SAAS,EAC7B,OAAM,uBACJ,UACA,mBACA,oBACD;;AAUL,KALE,SAAS,SAAS,kCAClB,SAAS,SAAS,+BAKlB,OAAM,iBAAiB,UAAU,oBAAoB;AAIvD,KAAI,SAAS,SAAS,kCACpB,OAAM,sBAAsB,UAAU,oBAAoB;AAI5D,KAAI,SAAS,SAAS,6BACpB,OAAM,iBAAiB,UAAU,oBAAoB;AAIvD,KAAI,SAAS,SAAS,UACpB,OAAM,SAAS,SAAS;AAI1B,0BAAyB,UAAU,qBAAqB,mBAAmB,CAAC;;;;;;;;AAS9E,SAAS,6CACP,UACA,cACyB;AACzB,KAAI,CAAC,aAAc,QAAO;CAE1B,MAAM,yCAAyB,IAAI,KAAa;AAChD,MAAK,MAAM,YAAY,2BAA2B;AAChD,MAAI,CAAC,yCAAyC,UAAU,SAAS,CAAE;AAEnE,OAAK,MAAM,cAAc,SAAS,MAAM,cAAc,aAAa,EAAE,CACnE,wBAAuB,IAAI,YAAY,WAAW,CAAC;;AAGvD,KAAI,uBAAuB,SAAS,EAAG,QAAO;CAE9C,MAAM,EAAE,eAAe,kBAAkB,gBAAgB,qBACvD,SAAS;CACX,MAAM,cAAc,kBAClB,eACA,kBACA,gBACA,kBACA,SAAS,SACT,SAAS,cACV;CAED,MAAM,yCAAyB,IAAI,KAAa;AAChD,MAAK,MAAM,iBAAiB,OAAO,OAAO,YAAY,CACpD,MAAK,MAAM,CAAC,YAAY,eAAe,OAAO,QAAQ,cAAc,CAClE,KAAI,uBAAuB,IAAI,WAAW,CACxC,wBAAuB,IAAI,WAAW;AAK5C,QAAO,IAAI,IACT,CAAC,GAAG,aAAa,CAAC,QACf,aAAa,CAAC,uBAAuB,IAAI,SAAS,CACpD,CACF"}
1
+ {"version":3,"file":"translate.js","names":[],"sources":["../../../src/cli/commands/translate.ts"],"sourcesContent":["import { EnqueueFilesResult } from 'generaltranslation/types';\nimport { TranslateFlags } from '../../types/index.js';\nimport { Settings } from '../../types/index.js';\nimport {\n FileTranslationData,\n runDownloadWorkflow,\n} from '../../workflows/download.js';\nimport { createFileMapping } from '../../formats/files/fileMapping.js';\nimport copyFile from '../../fs/copyFile.js';\nimport flattenJsonFiles from '../../utils/flattenJsonFiles.js';\nimport localizeStaticUrls from '../../utils/localizeStaticUrls.js';\nimport localizeRelativeAssets from '../../utils/localizeRelativeAssets.js';\nimport processAnchorIds from '../../utils/processAnchorIds.js';\nimport localizeStaticImports from '../../utils/localizeStaticImports.js';\nimport { postprocessMintlify } from '../../formats/files/postprocess/mintlify.js';\nimport { BranchData } from '../../types/branch.js';\nimport { getDownloadedMeta } from '../../state/recentDownloads.js';\nimport { persistPostProcessHashes } from '../../utils/persistPostprocessHashes.js';\nimport { runPublishWorkflow } from '../../workflows/publish.js';\nimport { SUPPORTED_FILE_EXTENSIONS } from '../../formats/files/supportedFiles.js';\nimport { hasNonIdentityFileFormatTransformForType } from '../../formats/files/transformFormat.js';\nimport { getRelative } from '../../fs/findFilepath.js';\nimport type { InlineLibrary } from '../../types/libraries.js';\n\n// Downloads translations that were completed\nexport async function handleTranslate(\n options: TranslateFlags,\n settings: Settings,\n fileVersionData: FileTranslationData | undefined,\n jobData: EnqueueFilesResult | undefined,\n branchData: BranchData | undefined,\n publishMap?: Map<string, boolean>,\n inlineLibrary?: InlineLibrary\n) {\n if (fileVersionData) {\n const {\n resolvedPaths,\n placeholderPaths,\n transformPaths,\n transformFormats,\n } = settings.files;\n\n const fileMapping = createFileMapping(\n resolvedPaths,\n placeholderPaths,\n transformPaths,\n transformFormats,\n settings.locales,\n settings.defaultLocale\n );\n // Check for remaining translations\n await runDownloadWorkflow({\n fileVersionData: fileVersionData,\n jobData: jobData,\n branchData: branchData,\n locales: settings.locales,\n timeoutDuration: options.timeout,\n resolveOutputPath: (sourcePath, locale) =>\n fileMapping[locale]?.[sourcePath] ?? null,\n options: settings,\n inlineLibrary,\n forceRetranslation: options.force,\n forceDownload: options.forceDownload || options.force, // if force is true should also force download\n });\n\n // Publish/unpublish files after translations are downloaded\n if (publishMap && branchData?.currentBranch.id) {\n const files = Object.entries(fileVersionData).map(([fileId, data]) => ({\n fileId,\n versionId: data.versionId,\n fileName: data.fileName,\n }));\n await runPublishWorkflow(\n files,\n publishMap,\n branchData.currentBranch.id,\n settings\n );\n }\n }\n}\n\nexport async function postProcessTranslations(\n settings: Settings,\n includeFiles?: Set<string>\n) {\n const postProcessIncludes = filterPostProcessIncludesForFormatTransforms(\n settings,\n includeFiles\n );\n if (includeFiles && postProcessIncludes?.size === 0) return;\n\n await postprocessMintlify(settings, postProcessIncludes);\n\n // Localize static urls (/docs -> /[locale]/docs) and preserve anchor IDs for non-default locales\n // Default locale is processed earlier in the flow in base.ts\n if (settings.options?.experimentalLocalizeStaticUrls) {\n const nonDefaultLocales = settings.locales.filter(\n (locale) => locale !== settings.defaultLocale\n );\n if (nonDefaultLocales.length > 0) {\n await localizeStaticUrls(\n settings,\n nonDefaultLocales,\n postProcessIncludes\n );\n }\n }\n\n // Rewrite relative asset URLs in translated md/mdx files\n if (settings.options?.experimentalLocalizeRelativeAssets) {\n const nonDefaultLocales = settings.locales.filter(\n (locale) => locale !== settings.defaultLocale\n );\n if (nonDefaultLocales.length > 0) {\n await localizeRelativeAssets(\n settings,\n nonDefaultLocales,\n postProcessIncludes\n );\n }\n }\n\n // Localize static imports (import Snippet from /snippets/file.mdx -> import Snippet from /snippets/[locale]/file.mdx)\n if (settings.options?.experimentalLocalizeStaticImports) {\n await localizeStaticImports(settings, postProcessIncludes);\n }\n\n const shouldProcessAnchorIds =\n settings.options?.experimentalLocalizeStaticUrls ||\n settings.options?.experimentalAddHeaderAnchorIds;\n\n // Add explicit anchor IDs to translated MDX/MD files to preserve navigation.\n // Uses inline {#id} format by default, or div wrapping if experimentalAddHeaderAnchorIds is 'mintlify'.\n //\n // Runs last of the md/mdx passes: the others re-stringify the document, which\n // re-indents headings nested in JSX and would otherwise move them out from\n // under the anchors placed here.\n if (shouldProcessAnchorIds) {\n await processAnchorIds(settings, postProcessIncludes);\n }\n\n // Flatten json files into a single file\n if (settings.options?.experimentalFlattenJsonFiles) {\n await flattenJsonFiles(settings, postProcessIncludes);\n }\n\n // Copy files to the target locale\n if (settings.options?.copyFiles) {\n await copyFile(settings);\n }\n\n // Record postprocessed content hashes for newly downloaded files\n persistPostProcessHashes(settings, postProcessIncludes, getDownloadedMeta());\n}\n\n/**\n * Exclude only outputs whose source file was translated into a different format.\n * @param settings - The settings for the project\n * @param includeFiles - The files to include in the post-processing\n * @returns The files to exclude in the post-processing\n */\nfunction filterPostProcessIncludesForFormatTransforms(\n settings: Settings,\n includeFiles?: Set<string>\n): Set<string> | undefined {\n if (!includeFiles) return includeFiles;\n\n const transformedSourcePaths = new Set<string>();\n for (const fileType of SUPPORTED_FILE_EXTENSIONS) {\n if (!hasNonIdentityFileFormatTransformForType(settings, fileType)) continue;\n\n for (const sourcePath of settings.files.resolvedPaths[fileType] || []) {\n transformedSourcePaths.add(getRelative(sourcePath));\n }\n }\n if (transformedSourcePaths.size === 0) return includeFiles;\n\n const { resolvedPaths, placeholderPaths, transformPaths, transformFormats } =\n settings.files;\n const fileMapping = createFileMapping(\n resolvedPaths,\n placeholderPaths,\n transformPaths,\n transformFormats,\n settings.locales,\n settings.defaultLocale\n );\n\n const transformedOutputPaths = new Set<string>();\n for (const localeMapping of Object.values(fileMapping)) {\n for (const [sourcePath, outputPath] of Object.entries(localeMapping)) {\n if (transformedSourcePaths.has(sourcePath)) {\n transformedOutputPaths.add(outputPath);\n }\n }\n }\n\n return new Set(\n [...includeFiles].filter(\n (filePath) => !transformedOutputPaths.has(filePath)\n )\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAyBA,eAAsB,gBACpB,SACA,UACA,iBACA,SACA,YACA,YACA,eACA;AACA,KAAI,iBAAiB;EACnB,MAAM,EACJ,eACA,kBACA,gBACA,qBACE,SAAS;EAEb,MAAM,cAAc,kBAClB,eACA,kBACA,gBACA,kBACA,SAAS,SACT,SAAS,cACV;AAED,QAAM,oBAAoB;GACP;GACR;GACG;GACZ,SAAS,SAAS;GAClB,iBAAiB,QAAQ;GACzB,oBAAoB,YAAY,WAC9B,YAAY,UAAU,eAAe;GACvC,SAAS;GACT;GACA,oBAAoB,QAAQ;GAC5B,eAAe,QAAQ,iBAAiB,QAAQ;GACjD,CAAC;AAGF,MAAI,cAAc,YAAY,cAAc,GAM1C,OAAM,mBALQ,OAAO,QAAQ,gBAAgB,CAAC,KAAK,CAAC,QAAQ,WAAW;GACrE;GACA,WAAW,KAAK;GAChB,UAAU,KAAK;GAChB,EAEM,EACL,YACA,WAAW,cAAc,IACzB,SACD;;;AAKP,eAAsB,wBACpB,UACA,cACA;CACA,MAAM,sBAAsB,6CAC1B,UACA,aACD;AACD,KAAI,gBAAgB,qBAAqB,SAAS,EAAG;AAErD,OAAM,oBAAoB,UAAU,oBAAoB;AAIxD,KAAI,SAAS,SAAS,gCAAgC;EACpD,MAAM,oBAAoB,SAAS,QAAQ,QACxC,WAAW,WAAW,SAAS,cACjC;AACD,MAAI,kBAAkB,SAAS,EAC7B,OAAM,mBACJ,UACA,mBACA,oBACD;;AAKL,KAAI,SAAS,SAAS,oCAAoC;EACxD,MAAM,oBAAoB,SAAS,QAAQ,QACxC,WAAW,WAAW,SAAS,cACjC;AACD,MAAI,kBAAkB,SAAS,EAC7B,OAAM,uBACJ,UACA,mBACA,oBACD;;AAKL,KAAI,SAAS,SAAS,kCACpB,OAAM,sBAAsB,UAAU,oBAAoB;AAa5D,KATE,SAAS,SAAS,kCAClB,SAAS,SAAS,+BASlB,OAAM,iBAAiB,UAAU,oBAAoB;AAIvD,KAAI,SAAS,SAAS,6BACpB,OAAM,iBAAiB,UAAU,oBAAoB;AAIvD,KAAI,SAAS,SAAS,UACpB,OAAM,SAAS,SAAS;AAI1B,0BAAyB,UAAU,qBAAqB,mBAAmB,CAAC;;;;;;;;AAS9E,SAAS,6CACP,UACA,cACyB;AACzB,KAAI,CAAC,aAAc,QAAO;CAE1B,MAAM,yCAAyB,IAAI,KAAa;AAChD,MAAK,MAAM,YAAY,2BAA2B;AAChD,MAAI,CAAC,yCAAyC,UAAU,SAAS,CAAE;AAEnE,OAAK,MAAM,cAAc,SAAS,MAAM,cAAc,aAAa,EAAE,CACnE,wBAAuB,IAAI,YAAY,WAAW,CAAC;;AAGvD,KAAI,uBAAuB,SAAS,EAAG,QAAO;CAE9C,MAAM,EAAE,eAAe,kBAAkB,gBAAgB,qBACvD,SAAS;CACX,MAAM,cAAc,kBAClB,eACA,kBACA,gBACA,kBACA,SAAS,SACT,SAAS,cACV;CAED,MAAM,yCAAyB,IAAI,KAAa;AAChD,MAAK,MAAM,iBAAiB,OAAO,OAAO,YAAY,CACpD,MAAK,MAAM,CAAC,YAAY,eAAe,OAAO,QAAQ,cAAc,CAClE,KAAI,uBAAuB,IAAI,WAAW,CACxC,wBAAuB,IAAI,WAAW;AAK5C,QAAO,IAAI,IACT,CAAC,GAAG,aAAa,CAAC,QACf,aAAa,CAAC,uBAAuB,IAAI,SAAS,CACpD,CACF"}
@@ -1 +1 @@
1
- export declare const PACKAGE_VERSION = "2.17.1";
1
+ export declare const PACKAGE_VERSION = "2.17.3";
@@ -1,5 +1,5 @@
1
1
  //#region src/generated/version.ts
2
- const PACKAGE_VERSION = "2.17.1";
2
+ const PACKAGE_VERSION = "2.17.3";
3
3
  //#endregion
4
4
  export { PACKAGE_VERSION };
5
5
 
@@ -1 +1 @@
1
- {"version":3,"file":"version.js","names":[],"sources":["../../src/generated/version.ts"],"sourcesContent":["// This file is auto-generated. Do not edit manually.\nexport const PACKAGE_VERSION = '2.17.1';\n"],"mappings":";AACA,MAAa,kBAAkB"}
1
+ {"version":3,"file":"version.js","names":[],"sources":["../../src/generated/version.ts"],"sourcesContent":["// This file is auto-generated. Do not edit manually.\nexport const PACKAGE_VERSION = '2.17.3';\n"],"mappings":";AACA,MAAa,kBAAkB"}
@@ -2,6 +2,12 @@ import type { AdditionalOptions } from '../types/index.js';
2
2
  type AnchorIdSettings = {
3
3
  options?: Pick<AdditionalOptions, 'experimentalAddHeaderAnchorIds'>;
4
4
  };
5
+ /** A source range on a single line, as 1-based inclusive/exclusive columns. */
6
+ interface ColumnRange {
7
+ line: number;
8
+ startColumn: number;
9
+ endColumn: number;
10
+ }
5
11
  /**
6
12
  * Represents a heading with its position and metadata
7
13
  */
@@ -10,9 +16,23 @@ export interface HeadingInfo {
10
16
  level: number;
11
17
  slug: string;
12
18
  position: number;
19
+ /** 1-based line the heading starts on. */
20
+ startLine: number;
21
+ /** 1-based line the heading ends on (differs from startLine for setext). */
22
+ endLine: number;
23
+ /** 1-based column of the heading marker; anything left of it is indentation. */
24
+ startColumn: number;
25
+ /** 1-based column just past the text, before any closing `##`; -1 if unknown. */
26
+ textEndColumn: number;
27
+ /** `id` attribute of a wrapper element already anchoring this heading. */
28
+ wrapperId: ColumnRange | null;
29
+ /** Whether the author wrote an explicit `{#id}`. */
30
+ explicit: boolean;
13
31
  }
14
32
  /**
15
- * Extracts heading information from content (read-only, no modifications)
33
+ * Extracts heading information from content (read-only, no modifications).
34
+ * Source and translation are matched by position, so both must parse the same
35
+ * way — the fallback extractor misses headings nested in JSX.
16
36
  */
17
37
  export declare function extractHeadingInfo(mdxContent: string): HeadingInfo[];
18
38
  /**
@@ -1,13 +1,11 @@
1
1
  import { logger } from "../console/logger.js";
2
- import { unified } from "unified";
3
- import remarkParse from "remark-parse";
4
- import remarkMdx from "remark-mdx";
5
- import remarkFrontmatter from "remark-frontmatter";
2
+ import { forEachLineOutsideCodeFences, mapLinesOutsideCodeFences, parseMdxTolerantly } from "./mdxAnchorSyntax.js";
6
3
  import { visit } from "unist-util-visit";
7
- import remarkStringify from "remark-stringify";
8
- import { escapeHtmlInTextNodes, normalizeCJKCharacters } from "gt-remark";
9
- import { decode } from "html-entities";
10
4
  //#region src/utils/addExplicitAnchorIds.ts
5
+ /** An ATX heading line, split into indentation, marker and text. */
6
+ const ATX_HEADING = /^([ \t]*)(#{1,6}[ \t]+)(.*)$/;
7
+ /** A trailing custom anchor ID, in either the plain or MDX-escaped form. */
8
+ const TRAILING_ANCHOR = /\s*(?:\\\{#[^}]+\\\}|\{#[^}]+\})\s*$/;
11
9
  /**
12
10
  * Generates a slug from heading text
13
11
  */
@@ -25,41 +23,31 @@ function extractHeadingText(heading) {
25
23
  return text;
26
24
  }
27
25
  /**
28
- * Simple line-by-line heading extractor that skips fenced code blocks.
29
- * Used as a fallback when MDX parsing fails.
26
+ * Line-by-line heading extractor used when MDX parsing fails outright.
30
27
  */
31
28
  function extractHeadingsWithFallback(mdxContent) {
32
29
  const headings = [];
33
- const lines = mdxContent.split("\n");
34
30
  let position = 0;
35
- let inFence = false;
36
- let fenceChar = null;
37
- for (const line of lines) {
38
- const fenceMatch = line.match(/^(\s*)(`{3,}|~{3,})/);
39
- if (fenceMatch) {
40
- const fenceString = fenceMatch[2];
41
- if (!inFence) {
42
- inFence = true;
43
- fenceChar = fenceString;
44
- } else if (fenceChar && fenceString[0] === fenceChar[0] && fenceString.length >= fenceChar.length) {
45
- inFence = false;
46
- fenceChar = null;
47
- }
48
- continue;
49
- }
50
- if (inFence) continue;
51
- const headingMatch = line.match(/^(#{1,6})\s+(.*)$/);
52
- if (!headingMatch) continue;
53
- const hashes = headingMatch[1];
54
- const rawText = headingMatch[2];
31
+ forEachLineOutsideCodeFences(mdxContent, (line, index) => {
32
+ const headingMatch = line.match(ATX_HEADING);
33
+ if (!headingMatch) return;
34
+ const [, indent, marker, rawText] = headingMatch;
55
35
  const { cleanedText, explicitId } = parseHeadingContent(rawText);
56
- if (cleanedText || explicitId) headings.push({
36
+ if (!cleanedText && !explicitId) return;
37
+ headings.push({
57
38
  text: cleanedText,
58
- level: hashes.length,
39
+ level: marker.trim().length,
59
40
  slug: explicitId ?? generateSlug(cleanedText),
60
- position: position++
41
+ position: position++,
42
+ startLine: index + 1,
43
+ endLine: index + 1,
44
+ startColumn: indent.length + 1,
45
+ textEndColumn: line.length + 1,
46
+ wrapperId: null,
47
+ explicit: explicitId !== void 0
61
48
  });
62
- }
49
+ });
50
+ assignUniqueSlugs(headings);
63
51
  return headings;
64
52
  }
65
53
  function parseHeadingContent(text) {
@@ -72,39 +60,72 @@ function parseHeadingContent(text) {
72
60
  };
73
61
  }
74
62
  /**
75
- * Checks if a heading is already wrapped in a div with id
63
+ * Suffixes repeated slugs `-2`, `-3`, ... as Mintlify does. Author-written IDs
64
+ * are never renumbered, only reserved.
65
+ */
66
+ function assignUniqueSlugs(headings) {
67
+ const used = new Set(headings.filter((heading) => heading.explicit).map((heading) => heading.slug));
68
+ for (const heading of headings) {
69
+ if (heading.explicit) continue;
70
+ const base = heading.slug || "section";
71
+ let slug = base;
72
+ let suffix = 1;
73
+ while (used.has(slug)) {
74
+ suffix += 1;
75
+ slug = `${base}-${suffix}`;
76
+ }
77
+ heading.slug = slug;
78
+ used.add(slug);
79
+ }
80
+ }
81
+ /**
82
+ * Finds the `id` of a wrapper element already anchoring this heading. Requiring
83
+ * the heading to be its only child rules out containers like `<Tab>`.
76
84
  */
77
- function hasExplicitId(heading, _ast) {
78
- const lastChild = heading.children[heading.children.length - 1];
79
- if (lastChild?.type === "text") return /(\{#[^}]+\}|\\\{#[^}]+\\\}|\[[^\]]+\])\s*$/.test(lastChild.value);
80
- return false;
85
+ function findWrapperId(heading, parent) {
86
+ if (!parent || parent.type !== "mdxJsxFlowElement") return null;
87
+ const element = parent;
88
+ if (element.children.length !== 1 || element.children[0] !== heading) return null;
89
+ const position = element.attributes.find((attribute) => attribute.type === "mdxJsxAttribute" && attribute.name === "id")?.position;
90
+ if (!position || position.start.line !== position.end.line) return null;
91
+ return {
92
+ line: position.start.line,
93
+ startColumn: position.start.column,
94
+ endColumn: position.end.column
95
+ };
81
96
  }
82
97
  /**
83
- * Extracts heading information from content (read-only, no modifications)
98
+ * Extracts heading information from content (read-only, no modifications).
99
+ * Source and translation are matched by position, so both must parse the same
100
+ * way — the fallback extractor misses headings nested in JSX.
84
101
  */
85
102
  function extractHeadingInfo(mdxContent) {
86
- const headings = [];
87
- let processedAst;
103
+ let ast;
88
104
  try {
89
- const parseProcessor = unified().use(remarkParse).use(remarkFrontmatter, ["yaml", "toml"]).use(remarkMdx);
90
- const ast = parseProcessor.parse(mdxContent);
91
- processedAst = parseProcessor.runSync(ast);
105
+ ast = parseMdxTolerantly(mdxContent);
92
106
  } catch {
93
107
  return extractHeadingsWithFallback(mdxContent);
94
108
  }
109
+ const headings = [];
95
110
  let position = 0;
96
- visit(processedAst, "heading", (heading) => {
111
+ visit(ast, "heading", (heading, _index, parent) => {
97
112
  const { cleanedText, explicitId } = parseHeadingContent(extractHeadingText(heading));
98
- if (cleanedText || explicitId) {
99
- const slug = explicitId ?? generateSlug(cleanedText);
100
- headings.push({
101
- text: cleanedText,
102
- level: heading.depth,
103
- slug,
104
- position: position++
105
- });
106
- }
113
+ if (!cleanedText && !explicitId) return;
114
+ const lastChild = heading.children[heading.children.length - 1];
115
+ headings.push({
116
+ text: cleanedText,
117
+ level: heading.depth,
118
+ slug: explicitId ?? generateSlug(cleanedText),
119
+ position: position++,
120
+ startLine: heading.position?.start.line ?? -1,
121
+ endLine: heading.position?.end.line ?? -1,
122
+ startColumn: heading.position?.start.column ?? 1,
123
+ textEndColumn: lastChild?.position?.end.column ?? heading.position?.end.column ?? -1,
124
+ wrapperId: findWrapperId(heading, parent),
125
+ explicit: explicitId !== void 0
126
+ });
107
127
  });
128
+ assignUniqueSlugs(headings);
108
129
  return headings;
109
130
  }
110
131
  /**
@@ -123,23 +144,28 @@ function addExplicitAnchorIds(translatedContent, sourceHeadingMap, settings, sou
123
144
  sourceHeadingMap.forEach((sourceHeading, index) => {
124
145
  const translatedHeading = translatedHeadings[index];
125
146
  if (translatedHeading && translatedHeading.level === sourceHeading.level) {
126
- idMappings.set(index, sourceHeading.slug);
147
+ idMappings.set(index, {
148
+ id: sourceHeading.slug,
149
+ explicit: sourceHeading.explicit
150
+ });
127
151
  addedIds.push({
128
152
  heading: translatedHeading.text,
129
153
  id: sourceHeading.slug
130
154
  });
131
155
  }
132
156
  });
133
- if (idMappings.size === 0) return {
134
- content: translatedContent,
135
- hasChanges: false,
136
- addedIds: []
137
- };
138
157
  const translatedIsMdx = translatedPath ? translatedPath.toLowerCase().endsWith(".mdx") : true;
139
158
  const shouldEscapeAnchors = fileTypeHint === "mdx" ? true : fileTypeHint === "md" ? false : translatedIsMdx;
140
- let content;
141
- if (useDivWrapping) content = applyDivWrappedIds(translatedContent, translatedHeadings, idMappings);
142
- else content = applyInlineIds(translatedContent, idMappings, shouldEscapeAnchors);
159
+ if (idMappings.size === 0) {
160
+ const content = useDivWrapping ? translatedContent : normalizeInlineAnchors(translatedContent, shouldEscapeAnchors);
161
+ return {
162
+ content,
163
+ hasChanges: content !== translatedContent,
164
+ addedIds: []
165
+ };
166
+ }
167
+ let content = applyAnchorIds(translatedContent, translatedHeadings, idMappings, useDivWrapping, shouldEscapeAnchors);
168
+ if (!useDivWrapping) content = normalizeInlineAnchors(content, shouldEscapeAnchors);
143
169
  return {
144
170
  content,
145
171
  hasChanges: content !== translatedContent,
@@ -147,149 +173,55 @@ function addExplicitAnchorIds(translatedContent, sourceHeadingMap, settings, sou
147
173
  };
148
174
  }
149
175
  /**
150
- * Adds inline {#id} syntax to headings (standard markdown approach)
176
+ * Writes anchor IDs onto the translated document, locating headings by parser
177
+ * line position rather than by text. Edits run bottom-up to keep lines valid.
151
178
  */
152
- function applyInlineIds(translatedContent, idMappings, escapeAnchors) {
153
- const escapeInlineAnchors = (content) => {
154
- if (!escapeAnchors) return content;
155
- return content.replace(/\{#([A-Za-z0-9-_]+)\}/g, (match, id, offset, str) => {
156
- if (offset > 0 && str[offset - 1] === "\\") return match;
157
- return `\\{#${id}\\}`;
158
- });
159
- };
160
- let processedAst;
161
- try {
162
- const parseProcessor = unified().use(remarkParse).use(remarkFrontmatter, ["yaml", "toml"]).use(remarkMdx);
163
- const ast = parseProcessor.parse(translatedContent);
164
- processedAst = parseProcessor.runSync(ast);
165
- } catch {
166
- return applyInlineIdsStringFallback(translatedContent, idMappings, escapeAnchors);
167
- }
168
- let headingIndex = 0;
169
- let actuallyModifiedContent = false;
170
- visit(processedAst, "heading", (heading) => {
171
- const id = idMappings.get(headingIndex);
172
- if (id) {
173
- if (hasExplicitId(heading, processedAst)) {
174
- if (escapeAnchors) {
175
- const lastChild = heading.children[heading.children.length - 1];
176
- if (lastChild?.type === "text") {
177
- const match = lastChild.value.match(/\{#([^}]+)\}\s*$/);
178
- const alreadyEscaped = lastChild.value.match(/\\\{#[^}]+\\\}\s*$/);
179
- if (match && !alreadyEscaped) {
180
- const anchorId = match[1];
181
- lastChild.value = `${lastChild.value.replace(/\s*\{#[^}]+\}\s*$/, "")} \\{#${anchorId}\\}`;
182
- actuallyModifiedContent = true;
183
- }
184
- }
185
- }
186
- headingIndex++;
187
- return;
188
- }
189
- const lastChild = heading.children[heading.children.length - 1];
190
- if (lastChild?.type === "text") lastChild.value += escapeAnchors ? ` \\{#${id}\\}` : ` {#${id}}`;
191
- else heading.children.push({
192
- type: "text",
193
- value: escapeAnchors ? ` \\{#${id}\\}` : ` {#${id}}`
194
- });
195
- actuallyModifiedContent = true;
196
- }
197
- headingIndex++;
198
- });
199
- if (!actuallyModifiedContent) return escapeInlineAnchors(translatedContent);
200
- try {
201
- const stringifyProcessor = unified().use(remarkFrontmatter, ["yaml", "toml"]).use(remarkMdx).use(normalizeCJKCharacters).use(escapeHtmlInTextNodes).use(remarkStringify, { handlers: { text(node) {
202
- return node.value;
203
- } } });
204
- const outTree = stringifyProcessor.runSync(processedAst);
205
- let content = stringifyProcessor.stringify(outTree);
206
- if (content.endsWith("\n") && !translatedContent.endsWith("\n")) content = content.slice(0, -1);
207
- if (translatedContent.startsWith("\n") && !content.startsWith("\n")) content = "\n" + content;
208
- return content;
209
- } catch {
210
- return translatedContent;
211
- }
212
- }
213
- /**
214
- * Fallback string-based inline ID application when AST parsing fails
215
- */
216
- function applyInlineIdsStringFallback(translatedContent, idMappings, escapeAnchors) {
217
- let headingIndex = 0;
218
- let inFence = false;
219
- let fenceChar = null;
220
- return translatedContent.split("\n").map((line) => {
221
- const fenceMatch = line.match(/^(\s*)(`{3,}|~{3,})/);
222
- if (fenceMatch) {
223
- const fenceString = fenceMatch[2];
224
- if (!inFence) {
225
- inFence = true;
226
- fenceChar = fenceString;
227
- } else if (fenceChar && fenceString[0] === fenceChar[0] && fenceString.length >= fenceChar.length) {
228
- inFence = false;
229
- fenceChar = null;
230
- }
231
- return line;
179
+ function applyAnchorIds(translatedContent, translatedHeadings, idMappings, useDivWrapping, escapeAnchors) {
180
+ const lines = translatedContent.split("\n");
181
+ const ordered = [...translatedHeadings].sort((a, b) => b.startLine - a.startLine);
182
+ for (const heading of ordered) {
183
+ const mapping = idMappings.get(heading.position);
184
+ if (!mapping) continue;
185
+ if (heading.startLine < 1 || heading.endLine > lines.length) continue;
186
+ const index = heading.startLine - 1;
187
+ if (!useDivWrapping || mapping.explicit) {
188
+ if (heading.textEndColumn < 1) continue;
189
+ const anchor = escapeAnchors && !mapping.explicit ? `\\{#${mapping.id}\\}` : `{#${mapping.id}}`;
190
+ const line = lines[index];
191
+ lines[index] = `${line.slice(0, heading.textEndColumn - 1).replace(TRAILING_ANCHOR, "")} ${anchor}${line.slice(heading.textEndColumn - 1)}`;
192
+ continue;
232
193
  }
233
- if (inFence) return line;
234
- const headingMatch = line.match(/^(#{1,6}\s+)(.*)$/);
235
- if (!headingMatch) return line;
236
- const prefix = headingMatch[1];
237
- const text = headingMatch[2];
238
- const id = idMappings.get(headingIndex++);
239
- if (!id) return line;
240
- const hasEscaped = /\\\{#[^}]+\\\}\s*$/.test(text);
241
- const hasUnescaped = /\{#[^}]+\}\s*$/.test(text);
242
- if (hasEscaped) return line;
243
- if (hasUnescaped) {
244
- if (!escapeAnchors) return line;
245
- return `${prefix}${text.replace(/\{#([^}]+)\}\s*$/, "\\{#$1\\}")}`;
194
+ if (heading.wrapperId) {
195
+ const { line, startColumn, endColumn } = heading.wrapperId;
196
+ const wrapper = lines[line - 1];
197
+ lines[line - 1] = wrapper.slice(0, startColumn - 1) + `id="${mapping.id}"` + wrapper.slice(endColumn - 1);
198
+ continue;
246
199
  }
247
- return `${prefix}${text}${escapeAnchors ? ` \\{#${id}\\}` : ` {#${id}}`}`;
248
- }).join("\n");
200
+ const indent = lines[index].slice(0, Math.max(0, heading.startColumn - 1));
201
+ const body = lines.slice(index, heading.endLine).map((line) => ` ${line}`);
202
+ lines.splice(index, heading.endLine - heading.startLine + 1, `${indent}<div id="${mapping.id}">`, ...body, `${indent}</div>`);
203
+ }
204
+ return lines.join("\n");
249
205
  }
250
206
  /**
251
- * Wraps headings in divs with IDs (Mintlify approach)
207
+ * Normalizes every inline anchor: escaped for MDX, bare for Markdown.
252
208
  */
253
- function applyDivWrappedIds(translatedContent, translatedHeadings, idMappings) {
254
- const lines = translatedContent.split("\n");
255
- const headingLines = [];
256
- lines.forEach((line, index) => {
257
- const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
258
- if (headingMatch) {
259
- const level = headingMatch[1].length;
260
- headingLines.push({
261
- line,
262
- level,
263
- index
264
- });
209
+ function normalizeInlineAnchors(content, escapeAnchors) {
210
+ return mapLinesOutsideCodeFences(content, (line) => {
211
+ const atx = line.match(ATX_HEADING);
212
+ if (!atx) return line;
213
+ const escaped = atx[3].match(/\\\{#([A-Za-z0-9_-]+)\\\}\s*$/);
214
+ const bare = atx[3].match(/(?<!\\)\{#([A-Za-z0-9_-]+)\}\s*$/);
215
+ if (escapeAnchors && bare) {
216
+ const text = atx[3].replace(TRAILING_ANCHOR, "");
217
+ return `${atx[1]}${atx[2]}${text} \\{#${bare[1]}\\}`;
265
218
  }
266
- });
267
- let content = translatedContent;
268
- const headingsToWrap = [];
269
- translatedHeadings.forEach((heading, position) => {
270
- const id = idMappings.get(position);
271
- if (id) {
272
- const matchingLine = headingLines.find((hl) => {
273
- return decode(hl.line.replace(/^#{1,6}\s+/, "").trim().replace(/\*\*(.*?)\*\*/g, "$1").replace(/\*(.*?)\*/g, "$1").replace(/`(.*?)`/g, "$1").replace(/\[(.*?)\]\(.*?\)/g, "$1").trim()).trim() === decode(heading.text).trim() && hl.level === heading.level;
274
- });
275
- if (matchingLine) headingsToWrap.push({
276
- originalLine: matchingLine.line,
277
- id
278
- });
219
+ if (!escapeAnchors && escaped) {
220
+ const text = atx[3].replace(TRAILING_ANCHOR, "");
221
+ return `${atx[1]}${atx[2]}${text} {#${escaped[1]}}`;
279
222
  }
223
+ return line;
280
224
  });
281
- if (headingsToWrap.length > 0) {
282
- const sortedHeadings = headingsToWrap.sort((a, b) => b.originalLine.length - a.originalLine.length);
283
- for (const heading of sortedHeadings) {
284
- if (content.includes(`<div id="${heading.id}">`)) continue;
285
- const escapedLine = heading.originalLine.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
286
- const headingPattern = new RegExp(`^${escapedLine}\\s*$`, "gm");
287
- content = content.replace(headingPattern, (match) => {
288
- return `<div id="${heading.id}">\n ${match.trim()}\n</div>\n`;
289
- });
290
- }
291
- }
292
- return content;
293
225
  }
294
226
  //#endregion
295
227
  export { addExplicitAnchorIds, extractHeadingInfo };