@jacobbubu/md-to-lark 1.6.0 → 1.7.1
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 +6 -3
- package/dist/commands/publish-md/args.js +65 -3
- package/dist/commands/publish-md/command.js +35 -9
- package/dist/index.js +1 -0
- package/dist/pipeline/hast-to-last.js +293 -10
- package/dist/pipeline/index.js +1 -0
- package/dist/pipeline/markdown/md-to-semantic-hast.js +665 -0
- package/dist/protocol/capabilities.js +31 -0
- package/dist/protocol/contract.js +253 -0
- package/dist/protocol/frontmatter.js +43 -0
- package/dist/protocol/index.js +3 -0
- package/dist/protocol/types.js +1 -0
- package/dist/publish/asset-adapter.js +4 -1
- package/dist/publish/image-size-manifest.js +133 -0
- package/dist/publish/lark-tree-validator.js +31 -0
- package/dist/publish/process-file.js +171 -10
- package/dist/publish/render-report.js +45 -0
- package/dist/publish/runtime.js +11 -1
- package/dist/publish/stage-cache.js +29 -6
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -56,8 +56,8 @@ LARK_DOCUMENT_BASE_URL="https://li.feishu.cn"
|
|
|
56
56
|
|
|
57
57
|
Notes:
|
|
58
58
|
|
|
59
|
-
- `--dry-run`
|
|
60
|
-
-
|
|
59
|
+
- Pure local `--dry-run` does not require Feishu credentials, `--folder`, or `--doc`.
|
|
60
|
+
- Real publishing requires either `--doc` or `LARK_FOLDER_TOKEN`/`--folder`, plus valid Feishu credentials.
|
|
61
61
|
|
|
62
62
|
The first run should use a built-in sample:
|
|
63
63
|
|
|
@@ -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
|
-
-
|
|
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> [--
|
|
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.',
|
|
@@ -33,7 +39,7 @@ function usage() {
|
|
|
33
39
|
' 5) If markdown has exactly one h1 and --title is not provided, that h1 becomes doc title, then removed from content, and remaining headings are promoted by one level.',
|
|
34
40
|
' 6) Stage cache layout per markdown: 00-source, 01-prepare, 02-hast, 03-last, 04-btt, 05-publish.',
|
|
35
41
|
' 7) Prepare stage can pre-download remote markdown images and optional yt-dlp URL lines.',
|
|
36
|
-
' 8)
|
|
42
|
+
' 8) In article-render/v1 protocol mode, leading YAML frontmatter is parsed for article_render configuration and removed from visible output. In legacy mode, unrecognized frontmatter follows the legacy preparation policy.',
|
|
37
43
|
' 9) Missing local asset files are skipped/degraded to text fallback; publish will not fail only because a referenced local path is absent.',
|
|
38
44
|
' 10) Relative local asset paths resolve against the markdown file directory by default; use --resource-base-dir to override that base.',
|
|
39
45
|
'',
|
|
@@ -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)
|
|
@@ -229,7 +285,7 @@ export function parsePublishMdArgs(argv, env = process.env) {
|
|
|
229
285
|
if (!inputPath.trim()) {
|
|
230
286
|
throw new Error('Input path is required. Use --input <file.md|dir>.');
|
|
231
287
|
}
|
|
232
|
-
if (!documentId && !folderToken) {
|
|
288
|
+
if (!dryRun && !documentId && !folderToken) {
|
|
233
289
|
throw new Error('Folder token is required when --doc is not provided. Use --folder or set LARK_FOLDER_TOKEN.');
|
|
234
290
|
}
|
|
235
291
|
const normalizedPresetPaths = presetPaths.map((presetPath) => presetPath.trim()).filter(Boolean);
|
|
@@ -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
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
const publishOptions = {
|
|
70
|
+
...options,
|
|
71
|
+
folderToken: options.folderToken?.trim() || env.LARK_FOLDER_TOKEN?.trim() || '',
|
|
72
|
+
dryRun: options.dryRun ?? false,
|
|
73
|
+
};
|
|
74
|
+
if (!publishOptions.dryRun && !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(
|
|
82
|
+
const runtime = buildPublishRuntime(publishOptions, env, markdownPresets);
|
|
74
83
|
logPublishRuntimeSummary(runtime, inputSet.markdownFiles.length, inputSet.mode);
|
|
75
|
-
const normalizedDocumentId =
|
|
76
|
-
const resolveTargetDocumentId =
|
|
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
|
|
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 (!
|
|
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,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
|
|
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
|
-
|
|
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')),
|
package/dist/pipeline/index.js
CHANGED
|
@@ -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';
|