@jacobbubu/md-to-lark 1.6.0 → 1.7.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
@@ -175,13 +175,16 @@ Guardrails:
175
175
  - Title derivation, title prefix, and single-H1 promotion
176
176
  - Local attachment and image detection with real upload
177
177
  - Programmatic image display width control through `imageSizeResolver`
178
+ - Versioned `article-render/v1` contracts with inheritance, capability negotiation, strict pre-publish validation, and render reports
179
+ - Semantic figures, tables, equations, callouts, and GFM footnotes rendered to native Lark block structures
180
+ - Automatic `assets/manifest.yml` image sizing in renderer-contract mode
178
181
  - Remote image download and standalone URL preparation
179
182
  - Mermaid `text-drawing` and `board` output paths
180
183
  - Opt-in single-dollar inline math parsing for LaTeX-heavy papers
181
184
  - Table width heuristics and numeric-column right alignment
182
185
  - Chinese Markdown formatting preset (`zh-format`)
183
186
  - Ordered preset composition from CLI and programmatic usage
184
- - Stage cache output from `00-source` to `05-publish`
187
+ - Legacy stage cache output from `00-source` to `05-publish`; protocol mode adds contract and semantic stages through `07-publish`
185
188
  - Programmatic access through `publishMdToLark`
186
189
 
187
190
  ## Where To Read Next
@@ -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>] [--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]',
3
+ 'Usage: npm run publish:md -- --input <file.md|dir> [--renderer lark] [--renderer-contract <path>] [--strict] [--render-report <path>] [--image-size-manifest <path>] [other options]',
4
4
  '',
5
5
  'Options:',
6
6
  ' --input Markdown file path, or directory path (publish all *.md recursively).',
@@ -10,6 +10,12 @@ function usage() {
10
10
  ' --preset Optional preset module path (js/mjs/cjs/ts) or built-in name (e.g. medium). Repeatable; presets run in the given order before publish pipeline.',
11
11
  ' --document-base-url Base URL used to build documentUrl results (for example https://li.feishu.cn).',
12
12
  ' --resource-base-dir Base directory used to resolve relative local image/file paths. Default: the current markdown file directory.',
13
+ ' --renderer Target renderer. Default: lark.',
14
+ ' --renderer-contract Explicit article-render/v1 contract path.',
15
+ ' --renderer-default-contract Fallback renderer contract path when no article contract is found.',
16
+ ' --strict Require a valid renderer contract and fail before publish on semantic errors.',
17
+ ' --render-report Write the article-render/v1 JSON report to this path.',
18
+ ' --image-size-manifest Explicit assets/manifest.yml path for image display sizing.',
13
19
  ' --folder Feishu folder token. Default: LARK_FOLDER_TOKEN from .env',
14
20
  ' --doc Existing Feishu document id (single-file only). If set, publish directly into this doc (and clear content first).',
15
21
  ' --download-remote-images Enable prepare-stage remote image pre-download + link rewrite.',
@@ -58,6 +64,12 @@ export function parsePublishMdArgs(argv, env = process.env) {
58
64
  const presetPaths = [];
59
65
  let documentBaseUrl = '';
60
66
  let resourceBaseDir = '';
67
+ let renderer = '';
68
+ let rendererContractPath = '';
69
+ let rendererDefaultContractPath = '';
70
+ let strict;
71
+ let renderReportPath = '';
72
+ let imageSizeManifestPath = '';
61
73
  let folderToken = (env.LARK_FOLDER_TOKEN ?? '').trim();
62
74
  let documentId;
63
75
  let downloadRemoteImages;
@@ -137,6 +149,50 @@ export function parsePublishMdArgs(argv, env = process.env) {
137
149
  i += 1;
138
150
  continue;
139
151
  }
152
+ if (arg === '--renderer') {
153
+ const value = argv[i + 1];
154
+ if (!value)
155
+ throw new Error('Missing value for --renderer.');
156
+ renderer = value;
157
+ i += 1;
158
+ continue;
159
+ }
160
+ if (arg === '--renderer-contract') {
161
+ const value = argv[i + 1];
162
+ if (!value)
163
+ throw new Error('Missing value for --renderer-contract.');
164
+ rendererContractPath = value;
165
+ i += 1;
166
+ continue;
167
+ }
168
+ if (arg === '--renderer-default-contract') {
169
+ const value = argv[i + 1];
170
+ if (!value)
171
+ throw new Error('Missing value for --renderer-default-contract.');
172
+ rendererDefaultContractPath = value;
173
+ i += 1;
174
+ continue;
175
+ }
176
+ if (arg === '--strict') {
177
+ strict = true;
178
+ continue;
179
+ }
180
+ if (arg === '--render-report') {
181
+ const value = argv[i + 1];
182
+ if (!value)
183
+ throw new Error('Missing value for --render-report.');
184
+ renderReportPath = value;
185
+ i += 1;
186
+ continue;
187
+ }
188
+ if (arg === '--image-size-manifest') {
189
+ const value = argv[i + 1];
190
+ if (!value)
191
+ throw new Error('Missing value for --image-size-manifest.');
192
+ imageSizeManifestPath = value;
193
+ i += 1;
194
+ continue;
195
+ }
140
196
  if (arg === '--doc') {
141
197
  const value = argv[i + 1];
142
198
  if (!value)
@@ -245,6 +301,12 @@ export function parsePublishMdArgs(argv, env = process.env) {
245
301
  : {}),
246
302
  ...(documentBaseUrl.trim() ? { documentBaseUrl: documentBaseUrl.trim() } : {}),
247
303
  ...(resourceBaseDir.trim() ? { resourceBaseDir: resourceBaseDir.trim() } : {}),
304
+ ...(renderer.trim() ? { renderer: renderer.trim() } : {}),
305
+ ...(rendererContractPath.trim() ? { rendererContractPath: rendererContractPath.trim() } : {}),
306
+ ...(rendererDefaultContractPath.trim() ? { rendererDefaultContractPath: rendererDefaultContractPath.trim() } : {}),
307
+ ...(strict === undefined ? {} : { strict }),
308
+ ...(renderReportPath.trim() ? { renderReportPath: renderReportPath.trim() } : {}),
309
+ ...(imageSizeManifestPath.trim() ? { imageSizeManifestPath: imageSizeManifestPath.trim() } : {}),
248
310
  folderToken,
249
311
  ...(documentId ? { documentId: documentId.trim() } : {}),
250
312
  ...(downloadRemoteImages === undefined ? {} : { downloadRemoteImages }),
@@ -1,10 +1,11 @@
1
- import { getPublishMdUsage, hasPublishMdHelpFlag, parsePublishMdArgs } from './args.js';
1
+ import { getPublishMdUsage, hasPublishMdHelpFlag, parsePublishMdArgs, } from './args.js';
2
2
  import { resolvePublishInputSet } from './input-resolver.js';
3
3
  import { loadMarkdownPresets } from './preset-loader.js';
4
4
  import { createDocument, listFolderChildren, normalizeDocumentId } from '../../lark/docx/ops.js';
5
5
  import { processSingleMarkdownFile } from '../../publish/process-file.js';
6
6
  import { buildPublishRuntime, logPublishRuntimeSummary } from '../../publish/runtime.js';
7
7
  import { sleep } from '../../shared/rate-limiter.js';
8
+ import { getRendererCapabilities, validateRendererContract } from '../../protocol/index.js';
8
9
  export { getPublishMdUsage, parsePublishMdArgs };
9
10
  function resolveMarkdownPresetRefs(options) {
10
11
  if (options.presetPaths && options.presetPaths.length > 0) {
@@ -65,19 +66,29 @@ function createFolderDocumentResolver(runtime, options) {
65
66
  };
66
67
  }
67
68
  export async function publishMdToLark(options, env = process.env) {
68
- const inputSet = await resolvePublishInputSet(options.inputPath);
69
- const markdownPresets = await loadMarkdownPresets(resolveMarkdownPresetRefs(options));
70
- if (options.documentId && inputSet.markdownFiles.length !== 1) {
69
+ const publishOptions = {
70
+ ...options,
71
+ folderToken: options.folderToken?.trim() || env.LARK_FOLDER_TOKEN?.trim() || '',
72
+ dryRun: options.dryRun ?? false,
73
+ };
74
+ if (!publishOptions.documentId && !publishOptions.folderToken) {
75
+ throw new Error('Folder token is required when documentId is not provided.');
76
+ }
77
+ const inputSet = await resolvePublishInputSet(publishOptions.inputPath);
78
+ const markdownPresets = await loadMarkdownPresets(resolveMarkdownPresetRefs(publishOptions));
79
+ if (publishOptions.documentId && inputSet.markdownFiles.length !== 1) {
71
80
  throw new Error('--doc only supports single markdown input file.');
72
81
  }
73
- const runtime = buildPublishRuntime(options, env, markdownPresets);
82
+ const runtime = buildPublishRuntime(publishOptions, env, markdownPresets);
74
83
  logPublishRuntimeSummary(runtime, inputSet.markdownFiles.length, inputSet.mode);
75
- const normalizedDocumentId = options.documentId ? normalizeDocumentId(options.documentId) : undefined;
76
- const resolveTargetDocumentId = options.dryRun || normalizedDocumentId ? undefined : createFolderDocumentResolver(runtime, options);
84
+ const normalizedDocumentId = publishOptions.documentId ? normalizeDocumentId(publishOptions.documentId) : undefined;
85
+ const resolveTargetDocumentId = publishOptions.dryRun || normalizedDocumentId ? undefined : createFolderDocumentResolver(runtime, publishOptions);
77
86
  const results = [];
78
87
  for (let index = 0; index < inputSet.markdownFiles.length; index += 1) {
79
88
  const markdownPath = inputSet.markdownFiles[index];
80
- const perFileOptions = normalizedDocumentId ? { ...options, documentId: normalizedDocumentId } : options;
89
+ const perFileOptions = normalizedDocumentId
90
+ ? { ...publishOptions, documentId: normalizedDocumentId }
91
+ : publishOptions;
81
92
  const result = await processSingleMarkdownFile({
82
93
  runtime,
83
94
  inputSet,
@@ -92,7 +103,7 @@ export async function publishMdToLark(options, env = process.env) {
92
103
  status: result.status,
93
104
  documentUrl: result.documentUrl,
94
105
  });
95
- if (!options.dryRun && index < inputSet.markdownFiles.length - 1 && runtime.publishCooldownMs > 0) {
106
+ if (!publishOptions.dryRun && index < inputSet.markdownFiles.length - 1 && runtime.publishCooldownMs > 0) {
96
107
  console.error(`[${index + 1}/${inputSet.markdownFiles.length}] Cooldown ${runtime.publishCooldownMs}ms before next markdown...`);
97
108
  await sleep(runtime.publishCooldownMs);
98
109
  }
@@ -104,6 +115,21 @@ export async function runPublishMdToLarkCli(argv, env = process.env) {
104
115
  console.log(getPublishMdUsage());
105
116
  return;
106
117
  }
118
+ if (argv.includes('--print-capabilities')) {
119
+ const rendererIndex = argv.indexOf('--renderer');
120
+ const renderer = rendererIndex >= 0 ? argv[rendererIndex + 1] : undefined;
121
+ console.log(JSON.stringify(getRendererCapabilities(renderer), null, 2));
122
+ return;
123
+ }
124
+ const validateIndex = argv.indexOf('--validate-contract');
125
+ if (validateIndex >= 0) {
126
+ const contractPath = argv[validateIndex + 1];
127
+ if (!contractPath)
128
+ throw new Error('Missing value for --validate-contract.');
129
+ const resolved = await validateRendererContract(contractPath);
130
+ console.log(JSON.stringify(resolved, null, 2));
131
+ return;
132
+ }
107
133
  const options = parsePublishMdArgs(argv, env);
108
134
  const results = await publishMdToLark(options, env);
109
135
  process.stdout.write(`${JSON.stringify(results, null, 2)}\n`);
package/dist/index.js CHANGED
@@ -1 +1,2 @@
1
1
  export { publishMdToLark } from './commands/publish-md/index.js';
2
+ export { getRendererCapabilities, parseArticleFrontmatter, resolveRendererContract, validateRendererContract, } from './protocol/index.js';
@@ -1,6 +1,7 @@
1
1
  import { toString } from 'hast-util-to-string';
2
2
  import { createDefaultImagePayload } from '../last/image-defaults.js';
3
3
  import { LAST_TEXTUAL_BLOCK_TYPE_SET } from '../last/textual-block-types.js';
4
+ import { parseDirectiveWidth } from './markdown/md-to-semantic-hast.js';
4
5
  const BLOCK_CONTAINER_TAGS = new Set([
5
6
  'article',
6
7
  'section',
@@ -18,6 +19,7 @@ function createContext(options = {}) {
18
19
  inlineCounter: 1,
19
20
  imageSizeContext: options.imageSizeContext ?? {},
20
21
  warnedImageSizeKeys: new Set(),
22
+ semanticTarget: options.semanticTarget ?? {},
21
23
  ...(options.imageSizeResolver ? { imageSizeResolver: options.imageSizeResolver } : {}),
22
24
  };
23
25
  }
@@ -170,19 +172,27 @@ function buildImageSizeResolverContext(ctx, image) {
170
172
  }
171
173
  return context;
172
174
  }
173
- function toPositiveRoundedWidth(value) {
175
+ function toPositiveRoundedWidth(value, maximum = 1000) {
174
176
  if (!Number.isFinite(value) || value <= 0)
175
177
  return undefined;
176
- return Math.max(1, Math.round(value));
178
+ return Math.min(maximum, Math.max(1, Math.round(value)));
177
179
  }
178
180
  function applyImageDisplaySize(ctx, image, block) {
179
181
  if (!ctx.imageSizeResolver || !image.sourceUrl) {
180
- return;
182
+ return false;
181
183
  }
182
184
  const size = ctx.imageSizeResolver(image.sourceUrl, buildImageSizeResolverContext(ctx, image));
183
185
  if (!size) {
184
- return;
186
+ return false;
185
187
  }
188
+ const applyAspectRatio = () => {
189
+ if (typeof size.aspectRatio === 'number' &&
190
+ Number.isFinite(size.aspectRatio) &&
191
+ size.aspectRatio > 0 &&
192
+ typeof block.payload.width === 'number') {
193
+ block.payload.height = Math.max(1, Math.round(block.payload.width / size.aspectRatio));
194
+ }
195
+ };
186
196
  const hasWidthRatio = typeof size.widthRatio === 'number';
187
197
  if (hasWidthRatio) {
188
198
  const ratio = size.widthRatio;
@@ -193,21 +203,25 @@ function applyImageDisplaySize(ctx, image, block) {
193
203
  const width = toPositiveRoundedWidth(baseWidth * ratio);
194
204
  if (width !== undefined) {
195
205
  block.payload.width = width;
206
+ applyAspectRatio();
196
207
  }
197
- return;
208
+ return true;
198
209
  }
199
210
  warnInvalidImageDisplaySize(ctx, `widthRatio:${image.sourceUrl}:${String(size.widthRatio)}`, `Ignoring invalid image widthRatio for ${image.sourceUrl}: ${String(size.widthRatio)}. Expected 0 < widthRatio <= 1.`);
200
211
  }
201
212
  if (typeof size.widthPx === 'number') {
202
- const width = toPositiveRoundedWidth(size.widthPx);
213
+ const maximum = createDefaultImagePayload(block.parentId ? ctx.blocks[block.parentId]?.type : undefined).width ?? 1000;
214
+ const width = toPositiveRoundedWidth(size.widthPx, maximum);
203
215
  if (width !== undefined) {
204
216
  block.payload.width = width;
205
- return;
217
+ applyAspectRatio();
218
+ return true;
206
219
  }
207
220
  warnInvalidImageDisplaySize(ctx, `widthPx:${image.sourceUrl}:${String(size.widthPx)}`, `Ignoring invalid image widthPx for ${image.sourceUrl}: ${String(size.widthPx)}. Expected widthPx > 0.`);
208
221
  }
222
+ return false;
209
223
  }
210
- function createImageBlock(ctx, parentId, sourceUrl, alt = null, title = null, linkHref = null) {
224
+ function createImageBlock(ctx, parentId, sourceUrl, alt = null, title = null, linkHref = null, displayOverride) {
211
225
  const blockId = nextBlockId(ctx);
212
226
  const parentBlock = ctx.blocks[parentId];
213
227
  const image = {
@@ -239,10 +253,81 @@ function createImageBlock(ctx, parentId, sourceUrl, alt = null, title = null, li
239
253
  if (Object.keys(selectorAttrs).length > 0) {
240
254
  blockBase.selector = { attrs: selectorAttrs };
241
255
  }
242
- applyImageDisplaySize(ctx, image, blockBase);
256
+ const resolverMatched = applyImageDisplaySize(ctx, image, blockBase);
257
+ const shouldApplyOverride = Boolean(displayOverride && (!displayOverride.fallback || !resolverMatched));
258
+ if (shouldApplyOverride && displayOverride?.widthRatio !== undefined) {
259
+ const ratio = displayOverride.widthRatio;
260
+ if (Number.isFinite(ratio) && ratio > 0 && ratio <= 1) {
261
+ const baseWidth = createDefaultImagePayload(parentBlock?.type).width ?? blockBase.payload.width ?? 0;
262
+ blockBase.payload.width = Math.max(1, Math.round(baseWidth * ratio));
263
+ }
264
+ }
265
+ else if (shouldApplyOverride && displayOverride?.widthPx !== undefined && displayOverride.widthPx > 0) {
266
+ const maximum = createDefaultImagePayload(parentBlock?.type).width ?? 1000;
267
+ blockBase.payload.width = Math.min(maximum, Math.max(1, Math.round(displayOverride.widthPx)));
268
+ }
269
+ if (shouldApplyOverride && displayOverride?.aspectRatio && blockBase.payload.width) {
270
+ blockBase.payload.height = Math.max(1, Math.round(blockBase.payload.width / displayOverride.aspectRatio));
271
+ }
272
+ if (displayOverride?.align)
273
+ blockBase.payload.align = displayOverride.align;
243
274
  addBlock(ctx, blockBase);
244
275
  return blockId;
245
276
  }
277
+ function attachSemanticMeta(ctx, blockId, role, semanticId) {
278
+ const block = ctx.blocks[blockId];
279
+ if (!block)
280
+ return;
281
+ block.selector = {
282
+ ...(block.selector ?? {}),
283
+ labels: [...(block.selector?.labels ?? []), role],
284
+ attrs: {
285
+ ...(block.selector?.attrs ?? {}),
286
+ semanticRole: role,
287
+ ...(semanticId ? { semanticId } : {}),
288
+ },
289
+ };
290
+ }
291
+ function normalizeCalloutColor(value) {
292
+ return value?.trim().toLowerCase().replaceAll('-', '_') || undefined;
293
+ }
294
+ function calloutStyleForElement(element, footnote) {
295
+ const target = footnote ? undefined : getStringProp(element, 'type');
296
+ const defaults = {
297
+ note: { backgroundColor: 'light_gray', borderColor: 'gray' },
298
+ info: { backgroundColor: 'light_blue', borderColor: 'blue' },
299
+ tip: { backgroundColor: 'light_green', borderColor: 'green' },
300
+ warning: { backgroundColor: 'light_yellow', borderColor: 'yellow' },
301
+ danger: { backgroundColor: 'light_red', borderColor: 'red' },
302
+ important: { backgroundColor: 'light_purple', borderColor: 'purple' },
303
+ };
304
+ const style = defaults[target ?? 'note'] ?? defaults.note;
305
+ return {
306
+ backgroundColor: style.backgroundColor,
307
+ borderColor: style.borderColor,
308
+ };
309
+ }
310
+ function createCalloutBlock(ctx, parentId, element, footnote) {
311
+ const blockId = nextBlockId(ctx);
312
+ const base = calloutStyleForElement(element, footnote);
313
+ const footnoteTarget = ctx.semanticTarget.footnotes;
314
+ const backgroundColor = footnote ? normalizeCalloutColor(footnoteTarget?.background_color ?? null) : undefined;
315
+ const borderColor = footnote ? normalizeCalloutColor(footnoteTarget?.border_color ?? null) : undefined;
316
+ addBlock(ctx, {
317
+ id: blockId,
318
+ type: 'callout',
319
+ parentId,
320
+ children: [],
321
+ payload: {
322
+ ...base,
323
+ ...(backgroundColor ? { backgroundColor: backgroundColor } : {}),
324
+ ...(borderColor ? { borderColor: borderColor } : {}),
325
+ ...(footnoteTarget?.icon ? { emojiId: footnoteTarget.icon } : {}),
326
+ },
327
+ });
328
+ attachSemanticMeta(ctx, blockId, footnote ? 'footnote' : 'callout', getStringProp(element, 'semanticId') ?? undefined);
329
+ return blockId;
330
+ }
246
331
  function createIframeBlock(ctx, parentId, url, iframeType) {
247
332
  const blockId = nextBlockId(ctx);
248
333
  addBlock(ctx, {
@@ -426,7 +511,7 @@ function parseInlineNodes(ctx, nodes, marks = createDefaultMarks()) {
426
511
  if (node.tagName === 'u') {
427
512
  nextMarks.underline = true;
428
513
  }
429
- if (node.tagName === 'a') {
514
+ if (node.tagName === 'a' || node.tagName === 'm2l-footnote-reference') {
430
515
  const href = getStringProp(node, 'href');
431
516
  nextMarks.link = href ? { url: href } : null;
432
517
  }
@@ -877,6 +962,189 @@ function convertParagraph(ctx, paragraph, parentId) {
877
962
  }
878
963
  return [createTextualBlock(ctx, 'text', parentId, parseInlineNodes(ctx, getChildren(paragraph)))];
879
964
  }
965
+ function collectSemanticImages(element) {
966
+ const images = [];
967
+ const visit = (node) => {
968
+ if (!isElement(node))
969
+ return;
970
+ const image = extractImageSourceFromElement(node);
971
+ if (image?.sourceUrl) {
972
+ images.push(image);
973
+ return;
974
+ }
975
+ for (const child of getChildren(node))
976
+ visit(child);
977
+ };
978
+ for (const child of getChildren(element))
979
+ visit(child);
980
+ return images;
981
+ }
982
+ function findDirectSemanticChild(element, tagName) {
983
+ return getChildren(element).find((child) => isElement(child) && child.tagName === tagName);
984
+ }
985
+ function createSemanticTextBlock(ctx, element, parentId, role, semanticId) {
986
+ const blockId = createTextualBlock(ctx, 'text', parentId, parseInlineNodes(ctx, getChildren(element)));
987
+ attachSemanticMeta(ctx, blockId, role, semanticId);
988
+ return blockId;
989
+ }
990
+ function parseSemanticAlign(element) {
991
+ return parseAlignValue(getStringProp(element, 'align'));
992
+ }
993
+ function semanticAnnotationIds(ctx, wrapper, parentId, semanticId) {
994
+ const result = {};
995
+ for (const role of ['caption', 'note', 'source']) {
996
+ const element = findDirectSemanticChild(wrapper, `m2l-${role}`);
997
+ if (element)
998
+ result[role] = createSemanticTextBlock(ctx, element, parentId, role, semanticId);
999
+ }
1000
+ return result;
1001
+ }
1002
+ function convertSemanticFigure(ctx, figure, parentId) {
1003
+ const semanticId = getStringProp(figure, 'semanticId') ?? undefined;
1004
+ const display = parseDirectiveWidth(getStringProp(figure, 'width') ?? '');
1005
+ const fallbackRatio = ctx.semanticTarget.figures?.default_width_ratio;
1006
+ const effectiveDisplay = display ??
1007
+ (typeof fallbackRatio === 'number' && fallbackRatio > 0 && fallbackRatio <= 1
1008
+ ? { widthRatio: fallbackRatio, fallback: true }
1009
+ : undefined);
1010
+ const align = parseSemanticAlign(figure);
1011
+ const imageIds = collectSemanticImages(figure).map((image) => {
1012
+ const imageId = createImageBlock(ctx, parentId, image.sourceUrl, image.alt, image.title, image.linkHref, {
1013
+ ...(effectiveDisplay ?? {}),
1014
+ ...(align ? { align } : {}),
1015
+ });
1016
+ attachSemanticMeta(ctx, imageId, 'figure-image', semanticId);
1017
+ return imageId;
1018
+ });
1019
+ const annotations = semanticAnnotationIds(ctx, figure, parentId, semanticId);
1020
+ const target = ctx.semanticTarget.figures;
1021
+ const ids = [];
1022
+ if (target?.caption_position === 'above' && annotations.caption)
1023
+ ids.push(annotations.caption);
1024
+ if (target?.source_position === 'above' && annotations.source)
1025
+ ids.push(annotations.source);
1026
+ if (target?.note_position === 'above' && annotations.note)
1027
+ ids.push(annotations.note);
1028
+ ids.push(...imageIds);
1029
+ if (target?.caption_position !== 'above' && annotations.caption)
1030
+ ids.push(annotations.caption);
1031
+ if (target?.source_position === 'below-caption' && annotations.source)
1032
+ ids.push(annotations.source);
1033
+ if (target?.note_position !== 'above' && annotations.note)
1034
+ ids.push(annotations.note);
1035
+ if (target?.source_position !== 'above' && target?.source_position !== 'below-caption' && annotations.source) {
1036
+ ids.push(annotations.source);
1037
+ }
1038
+ return ids;
1039
+ }
1040
+ function convertSemanticTable(ctx, wrapper, parentId) {
1041
+ const semanticId = getStringProp(wrapper, 'semanticId') ?? undefined;
1042
+ const table = getChildren(wrapper).find((child) => isElement(child) && child.tagName === 'table');
1043
+ const annotations = semanticAnnotationIds(ctx, wrapper, parentId, semanticId);
1044
+ const tableIds = table ? convertTable(ctx, table, parentId) : [];
1045
+ for (const tableId of tableIds)
1046
+ attachSemanticMeta(ctx, tableId, 'semantic-table', semanticId);
1047
+ const target = ctx.semanticTarget.tables;
1048
+ const ids = [];
1049
+ if (target?.caption_position !== 'below' && annotations.caption)
1050
+ ids.push(annotations.caption);
1051
+ if (target?.source_position === 'above' && annotations.source)
1052
+ ids.push(annotations.source);
1053
+ ids.push(...tableIds);
1054
+ if (target?.caption_position === 'below' && annotations.caption)
1055
+ ids.push(annotations.caption);
1056
+ if (annotations.note)
1057
+ ids.push(annotations.note);
1058
+ if (target?.source_position !== 'above' && annotations.source)
1059
+ ids.push(annotations.source);
1060
+ return ids;
1061
+ }
1062
+ function convertSemanticEquation(ctx, wrapper, parentId) {
1063
+ const semanticId = getStringProp(wrapper, 'semanticId') ?? undefined;
1064
+ const ids = [];
1065
+ const equationNumber = getStringProp(wrapper, 'equationNumber');
1066
+ let numberAttached = false;
1067
+ for (const child of getChildren(wrapper)) {
1068
+ if (isElement(child) && ['m2l-caption', 'm2l-note', 'm2l-source'].includes(child.tagName))
1069
+ continue;
1070
+ for (const blockId of convertBlock(ctx, child, parentId)) {
1071
+ attachSemanticMeta(ctx, blockId, 'equation', semanticId);
1072
+ const block = ctx.blocks[blockId];
1073
+ if (equationNumber && !numberAttached && block && isTextualBlockNode(block)) {
1074
+ block.payload.inlines.push({
1075
+ id: nextInlineId(ctx),
1076
+ kind: 'text_run',
1077
+ marks: createDefaultMarks(),
1078
+ text: ` (${equationNumber})`,
1079
+ });
1080
+ numberAttached = true;
1081
+ }
1082
+ ids.push(blockId);
1083
+ }
1084
+ }
1085
+ const annotations = semanticAnnotationIds(ctx, wrapper, parentId, semanticId);
1086
+ if (annotations.caption)
1087
+ ids.push(annotations.caption);
1088
+ if (annotations.note)
1089
+ ids.push(annotations.note);
1090
+ if (annotations.source)
1091
+ ids.push(annotations.source);
1092
+ return ids;
1093
+ }
1094
+ function prependFootnoteLabel(ctx, blockId, label) {
1095
+ const block = ctx.blocks[blockId];
1096
+ if (!block || !isTextualBlockNode(block))
1097
+ return;
1098
+ block.payload.inlines.unshift({
1099
+ id: nextInlineId(ctx),
1100
+ kind: 'text_run',
1101
+ marks: { ...createDefaultMarks(), bold: true },
1102
+ text: `[${label}] `,
1103
+ });
1104
+ }
1105
+ function convertSemanticCallout(ctx, element, parentId, footnote) {
1106
+ const output = [];
1107
+ let calloutId = createCalloutBlock(ctx, parentId, element, footnote);
1108
+ output.push(calloutId);
1109
+ let firstText = true;
1110
+ const appendConverted = (blockId) => {
1111
+ const block = ctx.blocks[blockId];
1112
+ if (!block)
1113
+ return;
1114
+ if (block.type === 'image' || block.type === 'table' || block.type === 'file' || block.type === 'board') {
1115
+ block.parentId = parentId;
1116
+ attachSemanticMeta(ctx, blockId, footnote ? 'footnote-media-sibling' : 'callout-media-sibling', getStringProp(element, 'semanticId') ?? undefined);
1117
+ output.push(blockId);
1118
+ calloutId = createCalloutBlock(ctx, parentId, element, footnote);
1119
+ output.push(calloutId);
1120
+ return;
1121
+ }
1122
+ const callout = ctx.blocks[calloutId];
1123
+ if (!callout || callout.type !== 'callout')
1124
+ return;
1125
+ block.parentId = calloutId;
1126
+ appendChild(callout, blockId);
1127
+ if (footnote && firstText && isTextualBlockNode(block)) {
1128
+ prependFootnoteLabel(ctx, blockId, getStringProp(element, 'semanticId') ?? '?');
1129
+ firstText = false;
1130
+ }
1131
+ };
1132
+ for (const child of getChildren(element)) {
1133
+ const current = ctx.blocks[calloutId];
1134
+ if (!current || current.type !== 'callout')
1135
+ continue;
1136
+ for (const blockId of convertBlock(ctx, child, calloutId))
1137
+ appendConverted(blockId);
1138
+ }
1139
+ for (const blockId of [...output]) {
1140
+ const block = ctx.blocks[blockId];
1141
+ if (block?.type === 'callout' && block.children.length === 0) {
1142
+ delete ctx.blocks[blockId];
1143
+ output.splice(output.indexOf(blockId), 1);
1144
+ }
1145
+ }
1146
+ return output;
1147
+ }
880
1148
  function convertBlock(ctx, node, parentId) {
881
1149
  if (isWhitespaceTextNode(node)) {
882
1150
  return [];
@@ -923,6 +1191,21 @@ function convertBlock(ctx, node, parentId) {
923
1191
  return [createDividerBlock(ctx, parentId)];
924
1192
  case 'table':
925
1193
  return convertTable(ctx, node, parentId);
1194
+ case 'm2l-figure':
1195
+ return convertSemanticFigure(ctx, node, parentId);
1196
+ case 'm2l-table':
1197
+ return convertSemanticTable(ctx, node, parentId);
1198
+ case 'm2l-equation':
1199
+ return convertSemanticEquation(ctx, node, parentId);
1200
+ case 'm2l-callout':
1201
+ return convertSemanticCallout(ctx, node, parentId, false);
1202
+ case 'm2l-footnote':
1203
+ return convertSemanticCallout(ctx, node, parentId, true);
1204
+ case 'm2l-unknown-directive':
1205
+ case 'm2l-caption':
1206
+ case 'm2l-note':
1207
+ case 'm2l-source':
1208
+ return convertUnknownElement(ctx, node, parentId);
926
1209
  case 'img':
927
1210
  return [
928
1211
  createImageBlock(ctx, parentId, getStringProp(node, 'src'), getStringProp(node, 'alt'), getStringProp(node, 'title')),
@@ -1,3 +1,4 @@
1
1
  export { markdownToHast } from './markdown/md-to-hast.js';
2
+ export { markdownToSemanticHast, parseDirectiveWidth } from './markdown/md-to-semantic-hast.js';
2
3
  export { prepareMarkdownBeforePublish } from './markdown/prepare-markdown.js';
3
4
  export { hastToLAST } from './hast-to-last.js';