@jacobbubu/md-to-lark 1.0.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.
Files changed (58) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +171 -0
  3. package/dist/btt/build-tree.js +79 -0
  4. package/dist/btt/index.js +1 -0
  5. package/dist/btt/types.js +1 -0
  6. package/dist/cli/publish-md-to-lark.js +15 -0
  7. package/dist/commands/publish-md/args.js +224 -0
  8. package/dist/commands/publish-md/command.js +97 -0
  9. package/dist/commands/publish-md/index.js +1 -0
  10. package/dist/commands/publish-md/input-resolver.js +48 -0
  11. package/dist/commands/publish-md/mermaid-render.js +17 -0
  12. package/dist/commands/publish-md/pipeline-transform.js +4 -0
  13. package/dist/commands/publish-md/preset-loader.js +113 -0
  14. package/dist/commands/publish-md/presets/medium.js +7 -0
  15. package/dist/commands/publish-md/presets/zh-format.js +8 -0
  16. package/dist/commands/publish-md/title-policy.js +93 -0
  17. package/dist/index.js +1 -0
  18. package/dist/interop/btt-to-last.js +79 -0
  19. package/dist/interop/codec-btt-to-last.js +435 -0
  20. package/dist/interop/codec-last-to-btt.js +383 -0
  21. package/dist/interop/codec-shared.js +722 -0
  22. package/dist/interop/index.js +2 -0
  23. package/dist/interop/last-to-btt.js +17 -0
  24. package/dist/lark/block-types.js +42 -0
  25. package/dist/lark/client.js +36 -0
  26. package/dist/lark/docx/ops.js +596 -0
  27. package/dist/lark/docx/render-btt.js +156 -0
  28. package/dist/lark/docx/render-models.js +1 -0
  29. package/dist/lark/docx/render-payload.js +338 -0
  30. package/dist/lark/docx/render-post-process.js +98 -0
  31. package/dist/lark/docx/render-table.js +87 -0
  32. package/dist/lark/docx/render-types.js +7 -0
  33. package/dist/lark/index.js +2 -0
  34. package/dist/lark/types.js +1 -0
  35. package/dist/last/api.js +1687 -0
  36. package/dist/last/index.js +3 -0
  37. package/dist/last/preview-terminal.js +296 -0
  38. package/dist/last/textual-block-types.js +19 -0
  39. package/dist/last/to-markdown.js +303 -0
  40. package/dist/last/types.js +11 -0
  41. package/dist/pipeline/hast-to-last.js +946 -0
  42. package/dist/pipeline/index.js +3 -0
  43. package/dist/pipeline/markdown/md-to-hast.js +34 -0
  44. package/dist/pipeline/markdown/prepare-markdown.js +1049 -0
  45. package/dist/preview/index.js +1 -0
  46. package/dist/preview/markdown-terminal.js +350 -0
  47. package/dist/publish/asset-adapter.js +123 -0
  48. package/dist/publish/btt-patch.js +65 -0
  49. package/dist/publish/common.js +139 -0
  50. package/dist/publish/ids.js +9 -0
  51. package/dist/publish/index.js +7 -0
  52. package/dist/publish/last-normalize.js +327 -0
  53. package/dist/publish/process-file.js +228 -0
  54. package/dist/publish/runtime.js +133 -0
  55. package/dist/publish/stage-cache.js +56 -0
  56. package/dist/shared/rate-limiter.js +18 -0
  57. package/dist/shared/retry.js +141 -0
  58. package/package.json +78 -0
@@ -0,0 +1,4 @@
1
+ export { applyStandaloneAttachmentTransforms } from '../../publish/asset-adapter.js';
2
+ export { patchBTTForMermaidAndAssets } from '../../publish/btt-patch.js';
3
+ export { buildPipelineDocumentId } from '../../publish/ids.js';
4
+ export { applyTableColumnWidthHeuristics, collectMermaidPatches, ensureLastBlockBttIds } from '../../publish/last-normalize.js';
@@ -0,0 +1,113 @@
1
+ import { stat } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { rewriteRelativeMediumAuthorLinks } from './presets/medium.js';
5
+ import { formatChineseMarkdown } from './presets/zh-format.js';
6
+ const BUILTIN_MARKDOWN_PRESETS = Object.freeze({
7
+ medium: {
8
+ transform: rewriteRelativeMediumAuthorLinks,
9
+ aliases: ['medium', 'builtin:medium', 'preset:medium'],
10
+ },
11
+ 'zh-format': {
12
+ transform: (markdown, context) => formatChineseMarkdown(markdown, context.inputPath),
13
+ aliases: [
14
+ 'zh-format',
15
+ 'zh-md-format',
16
+ 'builtin:zh-format',
17
+ 'preset:zh-format',
18
+ 'zh-smart-quotes',
19
+ 'cn-smart-quotes',
20
+ 'builtin:zh-smart-quotes',
21
+ 'preset:zh-smart-quotes',
22
+ ],
23
+ },
24
+ });
25
+ function toObjectRecord(value) {
26
+ return value && typeof value === 'object' ? value : null;
27
+ }
28
+ function pickTransform(moduleExports) {
29
+ const directCandidates = [
30
+ moduleExports.default,
31
+ moduleExports.transformMarkdown,
32
+ moduleExports.transform,
33
+ moduleExports.preset,
34
+ ];
35
+ for (const candidate of directCandidates) {
36
+ if (typeof candidate === 'function') {
37
+ return candidate;
38
+ }
39
+ const record = toObjectRecord(candidate);
40
+ if (!record)
41
+ continue;
42
+ if (typeof record.transformMarkdown === 'function') {
43
+ return record.transformMarkdown;
44
+ }
45
+ if (typeof record.transform === 'function') {
46
+ return record.transform;
47
+ }
48
+ }
49
+ return null;
50
+ }
51
+ function resolveBuiltInPreset(rawPreset) {
52
+ const normalized = rawPreset.trim().toLowerCase();
53
+ if (!normalized)
54
+ return null;
55
+ for (const [name, preset] of Object.entries(BUILTIN_MARKDOWN_PRESETS)) {
56
+ if (!preset.aliases.includes(normalized))
57
+ continue;
58
+ return {
59
+ sourcePath: `builtin:${name}`,
60
+ displayPath: `builtin:${name}`,
61
+ transform: preset.transform,
62
+ };
63
+ }
64
+ return null;
65
+ }
66
+ export function listBuiltinMarkdownPresetNames() {
67
+ return Object.keys(BUILTIN_MARKDOWN_PRESETS);
68
+ }
69
+ export async function loadMarkdownPreset(rawPath) {
70
+ const trimmed = (rawPath ?? '').trim();
71
+ if (!trimmed)
72
+ return null;
73
+ const builtin = resolveBuiltInPreset(trimmed);
74
+ if (builtin)
75
+ return builtin;
76
+ const absolutePath = path.resolve(trimmed);
77
+ let moduleStat;
78
+ try {
79
+ moduleStat = await stat(absolutePath);
80
+ }
81
+ catch {
82
+ const builtins = listBuiltinMarkdownPresetNames()
83
+ .map((name) => `"${name}"`)
84
+ .join(', ');
85
+ throw new Error(`Preset module not found: ${absolutePath}. Built-in presets: ${builtins}`);
86
+ }
87
+ if (!moduleStat.isFile()) {
88
+ throw new Error(`Preset module is not a file: ${absolutePath}`);
89
+ }
90
+ let importedModule;
91
+ try {
92
+ importedModule = (await import(pathToFileURL(absolutePath).href));
93
+ }
94
+ catch (error) {
95
+ throw new Error(`Failed to import preset module "${absolutePath}": ${error instanceof Error ? error.message : String(error)}`);
96
+ }
97
+ const transform = pickTransform(importedModule);
98
+ if (!transform) {
99
+ throw new Error(`Invalid preset module "${absolutePath}". Expected export: default function, transformMarkdown(), transform(), or preset object.`);
100
+ }
101
+ const wrappedTransform = async (markdown, context) => {
102
+ const next = await transform(markdown, context);
103
+ if (typeof next !== 'string') {
104
+ throw new Error(`Preset "${absolutePath}" must return a markdown string, got ${next === null ? 'null' : typeof next}.`);
105
+ }
106
+ return next;
107
+ };
108
+ return {
109
+ sourcePath: absolutePath,
110
+ displayPath: path.relative(process.cwd(), absolutePath) || absolutePath,
111
+ transform: wrappedTransform,
112
+ };
113
+ }
@@ -0,0 +1,7 @@
1
+ const RELATIVE_MEDIUM_AUTHOR_LINK_RE = /\]\((\/?@[^)\s]+)\)/g;
2
+ export function rewriteRelativeMediumAuthorLinks(markdown) {
3
+ return markdown.replace(RELATIVE_MEDIUM_AUTHOR_LINK_RE, (_matched, rawPath) => {
4
+ const normalizedPath = rawPath.startsWith('/') ? rawPath : `/${rawPath}`;
5
+ return `](https://medium.com${normalizedPath})`;
6
+ });
7
+ }
@@ -0,0 +1,8 @@
1
+ import { prettifyMarkdownContent } from '@jacobbubu/md-zh-format';
2
+ export async function formatChineseMarkdown(markdown, inputPath) {
3
+ const result = await prettifyMarkdownContent(markdown, inputPath, {
4
+ preserveFrontmatter: true,
5
+ promoteHeadings: false,
6
+ });
7
+ return result.prettifiedContent;
8
+ }
@@ -0,0 +1,93 @@
1
+ import path from 'node:path';
2
+ import { toString } from 'hast-util-to-string';
3
+ function stripMarkdownExtension(filePath) {
4
+ return filePath.replace(/\.md$/i, '');
5
+ }
6
+ function formatDatePrefix(now) {
7
+ const year = String(now.getFullYear());
8
+ const month = String(now.getMonth() + 1).padStart(2, '0');
9
+ const day = String(now.getDate()).padStart(2, '0');
10
+ return `${year}${month}${day}`;
11
+ }
12
+ function withDatePrefix(title, now = new Date()) {
13
+ const trimmed = title.trim();
14
+ const datePrefix = formatDatePrefix(now);
15
+ if (!trimmed)
16
+ return `${datePrefix}-untitled`;
17
+ if (new RegExp(`^${datePrefix}-`).test(trimmed))
18
+ return trimmed;
19
+ return `${datePrefix}-${trimmed}`;
20
+ }
21
+ function finalizeTitle(title, datePrefixEnabled) {
22
+ const trimmed = title.trim();
23
+ if (datePrefixEnabled) {
24
+ return withDatePrefix(trimmed);
25
+ }
26
+ return trimmed || 'untitled';
27
+ }
28
+ function isElementNode(node) {
29
+ return node.type === 'element';
30
+ }
31
+ function collectH1Hits(node, out) {
32
+ const children = Array.isArray(node.children) ? node.children : [];
33
+ for (let index = 0; index < children.length; index += 1) {
34
+ const child = children[index];
35
+ if (!child || !isElementNode(child))
36
+ continue;
37
+ if (child.tagName === 'h1') {
38
+ out.push({ parent: node, index, node: child });
39
+ }
40
+ collectH1Hits(child, out);
41
+ }
42
+ }
43
+ function promoteHeadingsOneLevel(node) {
44
+ const children = Array.isArray(node.children) ? node.children : [];
45
+ for (const child of children) {
46
+ if (!child || !isElementNode(child))
47
+ continue;
48
+ const headingMatch = /^h([2-9])$/.exec(child.tagName);
49
+ if (headingMatch) {
50
+ const currentLevel = Number(headingMatch[1] ?? 2);
51
+ child.tagName = `h${currentLevel - 1}`;
52
+ }
53
+ promoteHeadingsOneLevel(child);
54
+ }
55
+ }
56
+ export function applySingleH1TitleRule(hast) {
57
+ const h1Hits = [];
58
+ collectH1Hits(hast, h1Hits);
59
+ if (h1Hits.length !== 1)
60
+ return {};
61
+ const onlyHit = h1Hits[0];
62
+ if (!onlyHit)
63
+ return {};
64
+ const rawTitle = toString(onlyHit.node).trim();
65
+ const parentChildren = Array.isArray(onlyHit.parent.children) ? onlyHit.parent.children : [];
66
+ if (onlyHit.index >= 0 && onlyHit.index < parentChildren.length) {
67
+ parentChildren.splice(onlyHit.index, 1);
68
+ }
69
+ promoteHeadingsOneLevel(hast);
70
+ return rawTitle ? { derivedTitle: rawTitle } : {};
71
+ }
72
+ export function buildTitleForMarkdown(markdownPath, inputSet, titleOverride, h1DerivedTitle, options) {
73
+ const datePrefixEnabled = options?.datePrefix ?? true;
74
+ if (inputSet.mode === 'single') {
75
+ if (titleOverride && titleOverride.trim()) {
76
+ return finalizeTitle(titleOverride, datePrefixEnabled);
77
+ }
78
+ if (h1DerivedTitle && h1DerivedTitle.trim()) {
79
+ return finalizeTitle(h1DerivedTitle, datePrefixEnabled);
80
+ }
81
+ const baseName = path.basename(markdownPath, path.extname(markdownPath));
82
+ return finalizeTitle(baseName || 'md-to-lark', datePrefixEnabled);
83
+ }
84
+ const relative = stripMarkdownExtension(path.relative(inputSet.rootPath, markdownPath));
85
+ const relativeTitle = relative.split(path.sep).join(' / ');
86
+ if (titleOverride && titleOverride.trim()) {
87
+ return finalizeTitle(`${titleOverride.trim()} / ${relativeTitle}`, datePrefixEnabled);
88
+ }
89
+ if (h1DerivedTitle && h1DerivedTitle.trim()) {
90
+ return finalizeTitle(h1DerivedTitle, datePrefixEnabled);
91
+ }
92
+ return finalizeTitle(relativeTitle || path.basename(markdownPath, path.extname(markdownPath)), datePrefixEnabled);
93
+ }
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { publishMdToLark } from './commands/publish-md/index.js';
@@ -0,0 +1,79 @@
1
+ import { buildLASTIndexes, flattenTreeBlocks, fromRawBlockToLAST, } from './codec-btt-to-last.js';
2
+ import { normalizeDocumentIdToLAST } from './codec-last-to-btt.js';
3
+ export function convertBTTToLAST(bttDoc) {
4
+ const rawBlocks = Object.keys(bttDoc.flatBlocks ?? {}).length
5
+ ? Object.values(bttDoc.flatBlocks)
6
+ : flattenTreeBlocks(bttDoc.root);
7
+ const rawById = new Map();
8
+ for (const rawBlock of rawBlocks) {
9
+ rawById.set(String(rawBlock.block_id), rawBlock);
10
+ }
11
+ const rootRawId = String(bttDoc.rootBlockId || bttDoc.root.blockId);
12
+ if (!rawById.has(rootRawId)) {
13
+ throw new Error(`BTT root block "${rootRawId}" is not found in source blocks.`);
14
+ }
15
+ const orderedRawIds = [];
16
+ const visitedRawIds = new Set();
17
+ const visitRawId = (rawId) => {
18
+ if (visitedRawIds.has(rawId))
19
+ return;
20
+ const rawBlock = rawById.get(rawId);
21
+ if (!rawBlock)
22
+ return;
23
+ visitedRawIds.add(rawId);
24
+ orderedRawIds.push(rawId);
25
+ for (const childId of rawBlock.children ?? []) {
26
+ visitRawId(String(childId));
27
+ }
28
+ };
29
+ visitRawId(rootRawId);
30
+ for (const rawBlock of rawBlocks) {
31
+ const rawId = String(rawBlock.block_id);
32
+ if (!visitedRawIds.has(rawId)) {
33
+ orderedRawIds.push(rawId);
34
+ }
35
+ }
36
+ const bttToLastBlockId = {};
37
+ let blockCounter = 1;
38
+ for (const rawId of orderedRawIds) {
39
+ bttToLastBlockId[rawId] = `b_${blockCounter}`;
40
+ blockCounter += 1;
41
+ }
42
+ const inlineCounter = { value: 1 };
43
+ const blocks = {};
44
+ for (const rawId of orderedRawIds) {
45
+ const rawBlock = rawById.get(rawId);
46
+ if (!rawBlock)
47
+ continue;
48
+ const block = fromRawBlockToLAST(rawBlock, inlineCounter, bttToLastBlockId);
49
+ blocks[block.id] = block;
50
+ }
51
+ const rootId = bttToLastBlockId[rootRawId];
52
+ if (!rootId) {
53
+ throw new Error(`Failed to map root block "${rootRawId}" to LAST id.`);
54
+ }
55
+ if (!blocks[rootId]) {
56
+ throw new Error(`BTT rootBlockId "${rootId}" is not found in blocks.`);
57
+ }
58
+ const doc = {
59
+ schema: 'LAST',
60
+ version: '1.0.0',
61
+ id: normalizeDocumentIdToLAST(String(bttDoc.documentId)),
62
+ rootId,
63
+ blocks,
64
+ indexes: {
65
+ byType: {},
66
+ textScopes: {},
67
+ textScopeByBlockId: {},
68
+ },
69
+ };
70
+ doc.indexes = buildLASTIndexes(doc);
71
+ return doc;
72
+ }
73
+ export function summarizeBTTDocument(bttDoc) {
74
+ return {
75
+ totalBlocks: bttDoc.totalBlocks,
76
+ rootBlockId: bttDoc.rootBlockId,
77
+ missingChildren: bttDoc.missingChildren.length,
78
+ };
79
+ }