@jacobbubu/md-to-lark 1.4.10 → 1.5.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/README.md CHANGED
@@ -112,6 +112,7 @@ Presets, Mermaid, and stage artifacts:
112
112
  npm run publish:md -- --input ./test-md/comp/comp.md --preset medium --dry-run
113
113
  npm run publish:md -- --input ./test-md/comp/comp.md --preset zh-format --dry-run
114
114
  npm run publish:md -- --input ./test-md/comp/comp.md --preset zh-format --preset ./my-preset.mjs --dry-run
115
+ npm run publish:md -- --input ./paper.md --single-dollar-text-math --dry-run
115
116
  npm run publish:md -- --input ./tmp/generated/article.md --resource-base-dir ./source-assets --dry-run
116
117
  npm run publish:md -- --input ./test-md/mermaid.md --mermaid-target board --dry-run
117
118
  npm run publish:md -- --input ./test-md/comp/comp.md --pipeline-cache-dir ./out/debug-cache --dry-run
@@ -175,6 +176,7 @@ Guardrails:
175
176
  - Local attachment and image detection with real upload
176
177
  - Remote image download and standalone URL preparation
177
178
  - Mermaid `text-drawing` and `board` output paths
179
+ - Opt-in single-dollar inline math parsing for LaTeX-heavy papers
178
180
  - Table width heuristics and numeric-column right alignment
179
181
  - Chinese Markdown formatting preset (`zh-format`)
180
182
  - Ordered preset composition from CLI and programmatic usage
@@ -1,6 +1,6 @@
1
1
  function usage() {
2
2
  return [
3
- 'Usage: npm run publish:md -- --input <file.md|dir> [--title <doc_title_or_prefix>] [--date-prefix|--no-date-prefix] [--preset <preset_name_or_module_path>]... [--document-base-url <base_url>] [--resource-base-dir <dir>] [--folder <folder_token>] [--doc <document_id>] [--download-remote-images|--no-download-remote-images] [--yt-dlp-path <path>] [--yt-dlp-cookies-path <path>] [--pipeline-cache-dir <dir>] [--mermaid-target <text-drawing|board>] [--mermaid-board-syntax-type <int>] [--mermaid-board-style-type <int>] [--mermaid-board-diagram-type <int>] [--dry-run] [--help|-h]',
3
+ 'Usage: npm run publish:md -- --input <file.md|dir> [--title <doc_title_or_prefix>] [--date-prefix|--no-date-prefix] [--preset <preset_name_or_module_path>]... [--document-base-url <base_url>] [--resource-base-dir <dir>] [--folder <folder_token>] [--doc <document_id>] [--download-remote-images|--no-download-remote-images] [--yt-dlp-path <path>] [--yt-dlp-cookies-path <path>] [--pipeline-cache-dir <dir>] [--single-dollar-text-math] [--mermaid-target <text-drawing|board>] [--mermaid-board-syntax-type <int>] [--mermaid-board-style-type <int>] [--mermaid-board-diagram-type <int>] [--dry-run] [--help|-h]',
4
4
  '',
5
5
  'Options:',
6
6
  ' --input Markdown file path, or directory path (publish all *.md recursively).',
@@ -17,6 +17,7 @@ function usage() {
17
17
  ' --yt-dlp-path Optional yt-dlp executable path for standalone URL extraction.',
18
18
  ' --yt-dlp-cookies-path Optional cookie file path passed to yt-dlp --cookies.',
19
19
  ' --pipeline-cache-dir Pipeline cache root directory. Default: ./out/pipeline-cache',
20
+ ' --single-dollar-text-math Opt in to parsing $...$ as inline math. Default: disabled to avoid currency false positives.',
20
21
  ' --mermaid-target Mermaid render target: text-drawing (default) or board.',
21
22
  ' --mermaid-board-syntax-type Optional integer syntax_type for board createPlantuml (default: 2).',
22
23
  ' --mermaid-board-style-type Optional integer style_type for board createPlantuml.',
@@ -63,6 +64,7 @@ export function parsePublishMdArgs(argv, env = process.env) {
63
64
  let ytDlpPath = '';
64
65
  let ytDlpCookiesPath = '';
65
66
  let pipelineCacheDir = '';
67
+ let singleDollarTextMath;
66
68
  let mermaidTarget;
67
69
  let mermaidBoardSyntaxType;
68
70
  let mermaidBoardStyleType;
@@ -175,6 +177,10 @@ export function parsePublishMdArgs(argv, env = process.env) {
175
177
  i += 1;
176
178
  continue;
177
179
  }
180
+ if (arg === '--single-dollar-text-math' || arg === '--single-dollar-math') {
181
+ singleDollarTextMath = true;
182
+ continue;
183
+ }
178
184
  if (arg === '--mermaid-target') {
179
185
  const value = argv[i + 1];
180
186
  if (!value)
@@ -245,6 +251,7 @@ export function parsePublishMdArgs(argv, env = process.env) {
245
251
  ...(ytDlpPath.trim() ? { ytDlpPath: ytDlpPath.trim() } : {}),
246
252
  ...(ytDlpCookiesPath.trim() ? { ytDlpCookiesPath: ytDlpCookiesPath.trim() } : {}),
247
253
  ...(pipelineCacheDir.trim() ? { pipelineCacheDir: pipelineCacheDir.trim() } : {}),
254
+ ...(singleDollarTextMath === undefined ? {} : { singleDollarTextMath }),
248
255
  ...(mermaidTarget ? { mermaidTarget } : {}),
249
256
  ...(mermaidBoardSyntaxType === undefined ? {} : { mermaidBoardSyntaxType }),
250
257
  ...(mermaidBoardStyleType === undefined ? {} : { mermaidBoardStyleType }),
@@ -73,9 +73,7 @@ export async function publishMdToLark(options, env = process.env) {
73
73
  const runtime = buildPublishRuntime(options, env, markdownPresets);
74
74
  logPublishRuntimeSummary(runtime, inputSet.markdownFiles.length, inputSet.mode);
75
75
  const normalizedDocumentId = options.documentId ? normalizeDocumentId(options.documentId) : undefined;
76
- const resolveTargetDocumentId = options.dryRun || normalizedDocumentId
77
- ? undefined
78
- : createFolderDocumentResolver(runtime, options);
76
+ const resolveTargetDocumentId = options.dryRun || normalizedDocumentId ? undefined : createFolderDocumentResolver(runtime, options);
79
77
  const results = [];
80
78
  for (let index = 0; index < inputSet.markdownFiles.length; index += 1) {
81
79
  const markdownPath = inputSet.markdownFiles[index];
@@ -1,4 +1,4 @@
1
1
  export { applyStandaloneAttachmentTransforms } from '../../publish/asset-adapter.js';
2
2
  export { patchBTTForMermaidAndAssets } from '../../publish/btt-patch.js';
3
3
  export { buildPipelineDocumentId } from '../../publish/ids.js';
4
- export { applyTableColumnWidthHeuristics, collectMermaidPatches, ensureLastBlockBttIds } from '../../publish/last-normalize.js';
4
+ export { applyTableColumnWidthHeuristics, collectMermaidPatches, ensureLastBlockBttIds, } from '../../publish/last-normalize.js';
@@ -1,4 +1,4 @@
1
- import { buildLASTIndexes, flattenTreeBlocks, fromRawBlockToLAST, } from './codec-btt-to-last.js';
1
+ import { buildLASTIndexes, flattenTreeBlocks, fromRawBlockToLAST } from './codec-btt-to-last.js';
2
2
  import { normalizeDocumentIdToLAST } from './codec-last-to-btt.js';
3
3
  export function convertBTTToLAST(bttDoc) {
4
4
  const rawBlocks = Object.keys(bttDoc.flatBlocks ?? {}).length
@@ -1,5 +1,5 @@
1
- import { createDocumentChildren, } from './ops.js';
2
- import { DEFAULT_MERMAID_RENDER_CONFIG, } from './render-types.js';
1
+ import { createDocumentChildren } from './ops.js';
2
+ import { DEFAULT_MERMAID_RENDER_CONFIG } from './render-types.js';
3
3
  import { applyCreatedBoardMermaid, applyCreatedFileBlock, applyCreatedImageBlock } from './render-post-process.js';
4
4
  import { renderCreatedTableNode } from './render-table.js';
5
5
  import { buildCreatePayloadFromRawBlock, createRenderBatchEntry, getSourceBlockId, toObjectRecord, } from './render-payload.js';
@@ -124,7 +124,8 @@ export function canUseElementsOnlyPatch(rawBlock) {
124
124
  if (effectiveEntries.length === 0)
125
125
  return true;
126
126
  for (const [key, value] of effectiveEntries) {
127
- if (key === 'align' && (value === 1 || value === 2 || value === 3 || value === 'left' || value === 'center' || value === 'right')) {
127
+ if (key === 'align' &&
128
+ (value === 1 || value === 2 || value === 3 || value === 'left' || value === 'center' || value === 'right')) {
128
129
  continue;
129
130
  }
130
131
  return false;
@@ -310,7 +311,12 @@ export function getSourceBlockId(node, rawBlockRecord) {
310
311
  : node.blockId;
311
312
  }
312
313
  function isBatchSafeBlockType(blockType) {
313
- if (blockType === 1 || blockType === 23 || blockType === 27 || blockType === 31 || blockType === 32 || blockType === 43) {
314
+ if (blockType === 1 ||
315
+ blockType === 23 ||
316
+ blockType === 27 ||
317
+ blockType === 31 ||
318
+ blockType === 32 ||
319
+ blockType === 43) {
314
320
  return false;
315
321
  }
316
322
  return true;
@@ -33,7 +33,9 @@ export async function applyCreatedImageBlock(client, documentId, createdBlockId,
33
33
  ...(typeof width === 'number' ? { width } : {}),
34
34
  ...(typeof proportionalHeight === 'number' ? { height: proportionalHeight } : {}),
35
35
  ...(image && typeof image.align === 'number' ? { align: image.align } : {}),
36
- ...(image && toObjectRecord(image.caption) ? { caption: toObjectRecord(image.caption) } : {}),
36
+ ...(image && toObjectRecord(image.caption)
37
+ ? { caption: toObjectRecord(image.caption) }
38
+ : {}),
37
39
  ...(image && typeof image.scale === 'number' ? { scale: image.scale } : {}),
38
40
  };
39
41
  let imageToken = await uploadBinaryToNode(client, 'docx_image', createdBlockId, localPath, authOptions, mediaLimiter);
@@ -223,6 +223,36 @@ function mergeAdjacentTextRuns(inlines) {
223
223
  }
224
224
  return merged;
225
225
  }
226
+ function trimBoundaryNewlinesFromInlines(inlines) {
227
+ const trimmed = inlines.map((inline) => ({ ...inline }));
228
+ let start = 0;
229
+ let end = trimmed.length;
230
+ while (start < end) {
231
+ const inline = trimmed[start];
232
+ if (!inline || inline.kind !== 'text_run')
233
+ break;
234
+ const nextText = (inline.text ?? '').replace(/^(?:\r?\n)+/, '');
235
+ if (nextText.length === 0) {
236
+ start += 1;
237
+ continue;
238
+ }
239
+ inline.text = nextText;
240
+ break;
241
+ }
242
+ while (end > start) {
243
+ const inline = trimmed[end - 1];
244
+ if (!inline || inline.kind !== 'text_run')
245
+ break;
246
+ const nextText = (inline.text ?? '').replace(/(?:\r?\n)+$/, '');
247
+ if (nextText.length === 0) {
248
+ end -= 1;
249
+ continue;
250
+ }
251
+ inline.text = nextText;
252
+ break;
253
+ }
254
+ return mergeAdjacentTextRuns(trimmed.slice(start, end));
255
+ }
226
256
  function hasClassName(element, expected) {
227
257
  return getClassNames(element).includes(expected);
228
258
  }
@@ -674,17 +704,7 @@ function convertTable(ctx, table, parentId) {
674
704
  return [tableId];
675
705
  }
676
706
  function convertBlockquote(ctx, blockquote, parentId) {
677
- const quoteText = trimBoundaryNewlines(toString(blockquote));
678
- const inlines = quoteText.length
679
- ? [
680
- {
681
- id: nextInlineId(ctx),
682
- kind: 'text_run',
683
- marks: createDefaultMarks(),
684
- text: quoteText,
685
- },
686
- ]
687
- : [];
707
+ const inlines = trimBoundaryNewlinesFromInlines(parseInlineNodes(ctx, getChildren(blockquote)));
688
708
  return [createTextualBlock(ctx, 'quote', parentId, inlines)];
689
709
  }
690
710
  function trimBoundaryNewlines(value) {
@@ -4,12 +4,12 @@ import remarkGfm from 'remark-gfm';
4
4
  import remarkMath from 'remark-math';
5
5
  import remarkRehype from 'remark-rehype';
6
6
  import { normalizeMarkdownBeforeParse } from './normalize-markdown.js';
7
- export async function markdownToHast(markdown) {
7
+ export async function markdownToHast(markdown, options = {}) {
8
8
  const content = normalizeMarkdownBeforeParse(markdown);
9
9
  const processor = unified()
10
10
  .use(remarkParse)
11
11
  .use(remarkGfm)
12
- .use(remarkMath, { singleDollarTextMath: false })
12
+ .use(remarkMath, { singleDollarTextMath: options.singleDollarTextMath ?? false })
13
13
  .use(remarkRehype, { allowDangerousHtml: false });
14
14
  const mdast = processor.parse(content);
15
15
  const hast = await processor.run(mdast);
@@ -345,6 +345,7 @@ export function renderHASTToTerminal(root, options = {}) {
345
345
  return `${ctx.lines.join('\n')}\n`;
346
346
  }
347
347
  export async function renderMarkdownToTerminal(markdown, options = {}) {
348
- const hast = await markdownToHast(markdown);
348
+ const parseOptions = options.singleDollarTextMath === undefined ? {} : { singleDollarTextMath: options.singleDollarTextMath };
349
+ const hast = await markdownToHast(markdown, parseOptions);
349
350
  return renderHASTToTerminal(hast, options);
350
351
  }
@@ -44,7 +44,9 @@ function applyRawBlockPatch(rawBlock, blockId, mermaidByBlockId, assetByBlockId,
44
44
  file.media_kind = asset.mediaKind;
45
45
  }
46
46
  if (typeof file.view_type !== 'number') {
47
- file.view_type = shouldUsePreviewView(path.extname(asset.fileName).toLowerCase(), asset.mediaKind ?? 'file') ? 2 : 1;
47
+ file.view_type = shouldUsePreviewView(path.extname(asset.fileName).toLowerCase(), asset.mediaKind ?? 'file')
48
+ ? 2
49
+ : 1;
48
50
  }
49
51
  rawBlock.file = file;
50
52
  }
@@ -2,6 +2,6 @@ export { applyStandaloneAttachmentTransforms } from './asset-adapter.js';
2
2
  export { patchBTTForMermaidAndAssets } from './btt-patch.js';
3
3
  export { buildPipelineDocumentId } from './ids.js';
4
4
  export { applyTableColumnWidthHeuristics, collectMermaidPatches, ensureLastBlockBttIds } from './last-normalize.js';
5
- export { processSingleMarkdownFile } from './process-file.js';
6
- export { buildPublishRuntime, logPublishRuntimeSummary } from './runtime.js';
5
+ export { processSingleMarkdownFile, } from './process-file.js';
6
+ export { buildPublishRuntime, logPublishRuntimeSummary, } from './runtime.js';
7
7
  export { buildPipelineStagePaths, ensureDir, writeJson, writeSourceStage, writePrepareStage, writePrepareLogFile, writeHastStage, writeLastStage, writeBttStage, writePublishStageArtifact, } from './stage-cache.js';
@@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { convertLASTToBTT } from '../interop/index.js';
4
4
  import { clearDocumentContent, normalizeDocumentId } from '../lark/docx/ops.js';
5
- import { renderBTTToDocument, } from '../lark/docx/render-btt.js';
5
+ import { renderBTTToDocument } from '../lark/docx/render-btt.js';
6
6
  import { hastToLAST, markdownToHast, prepareMarkdownBeforePublish } from '../pipeline/index.js';
7
7
  import { applySingleH1TitleRule, buildTitleForMarkdown } from '../commands/publish-md/title-policy.js';
8
8
  import { applyStandaloneAttachmentTransforms } from './asset-adapter.js';
@@ -95,7 +95,7 @@ export async function processSingleMarkdownFile(params) {
95
95
  markdown = prepareResult.preparedContent;
96
96
  await writePrepareStage(stagePaths, markdown, prepareResult);
97
97
  console.error(`[prepare ${index + 1}/${inputSet.markdownFiles.length}] rewritten=${prepareResult.rewrittenCount} downloaded=${prepareResult.downloadedCount} failed=${prepareResult.failedCount} log=${prepareResult.logFilePath}`);
98
- const hast = await markdownToHast(markdown);
98
+ const hast = await markdownToHast(markdown, runtime.markdownParseConfig);
99
99
  await writeHastStage(stagePaths, hast);
100
100
  const h1RuleResult = options.title ? {} : applySingleH1TitleRule(hast);
101
101
  const title = buildTitleForMarkdown(markdownPath, inputSet, options.title, h1RuleResult.derivedTitle, {
@@ -99,6 +99,9 @@ export function buildPublishRuntime(options, env, markdownPresets) {
99
99
  ...(mermaidBoardDiagramType === undefined ? {} : { diagramType: mermaidBoardDiagramType }),
100
100
  },
101
101
  };
102
+ const markdownParseConfig = {
103
+ singleDollarTextMath: options.singleDollarTextMath ?? false,
104
+ };
102
105
  return {
103
106
  env,
104
107
  markdownPresets,
@@ -118,6 +121,7 @@ export function buildPublishRuntime(options, env, markdownPresets) {
118
121
  downloadRemoteImages,
119
122
  ...(ytDlpPath ? { ytDlpPath } : {}),
120
123
  mermaidRenderConfig,
124
+ markdownParseConfig,
121
125
  prepareConfig: {
122
126
  enabled: downloadRemoteImages,
123
127
  timeoutMs: prepareTimeoutMs,
@@ -138,6 +142,9 @@ export function logPublishRuntimeSummary(runtime, inputCount, inputMode) {
138
142
  console.error(runtime.mermaidRenderConfig.target === 'board'
139
143
  ? `Mermaid: target=board syntax_type=${String(runtime.mermaidRenderConfig.board.syntaxType)} style_type=${String(runtime.mermaidRenderConfig.board.styleType ?? '(default)')} diagram_type=${String(runtime.mermaidRenderConfig.board.diagramType ?? '(default)')}`
140
144
  : 'Mermaid: target=text-drawing');
145
+ if (runtime.markdownParseConfig.singleDollarTextMath) {
146
+ console.error('Markdown parse: single_dollar_text_math=true');
147
+ }
141
148
  console.error(`Document URL base: ${runtime.documentBaseUrl}`);
142
149
  if (runtime.resourceBaseDir) {
143
150
  console.error(`Local asset base override: ${runtime.resourceBaseDir}`);
@@ -1,9 +1,7 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
4
- const JPEG_SOF_MARKERS = new Set([
5
- 0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf,
6
- ]);
4
+ const JPEG_SOF_MARKERS = new Set([0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf]);
7
5
  function isPositiveInteger(value) {
8
6
  return Number.isInteger(value) && value > 0;
9
7
  }
@@ -68,9 +66,7 @@ function readJpegDimensions(buffer) {
68
66
  return null;
69
67
  }
70
68
  function readWebpDimensions(buffer) {
71
- if (buffer.length < 30 ||
72
- buffer.toString('ascii', 0, 4) !== 'RIFF' ||
73
- buffer.toString('ascii', 8, 12) !== 'WEBP') {
69
+ if (buffer.length < 30 || buffer.toString('ascii', 0, 4) !== 'RIFF' || buffer.toString('ascii', 8, 12) !== 'WEBP') {
74
70
  return null;
75
71
  }
76
72
  const chunkType = buffer.toString('ascii', 12, 16);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacobbubu/md-to-lark",
3
- "version": "1.4.10",
3
+ "version": "1.5.0",
4
4
  "description": "Publish Markdown to Feishu docs with a stable pipeline.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",