confluence-md-sync 0.11.1 → 0.11.2
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/dist/export/export-page.d.ts +2 -0
- package/dist/export/export-page.js +1 -0
- package/dist/export/links.d.ts +12 -0
- package/dist/export/links.js +42 -0
- package/dist/export/to-markdown.d.ts +3 -0
- package/dist/export/to-markdown.js +5 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/markdown/render.d.ts +2 -0
- package/dist/markdown/render.js +6 -2
- package/package.json +1 -1
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
import type { ConfluenceConfig } from '../client/config.js';
|
|
6
6
|
import type { MacroRegistry } from '../macros/registry.js';
|
|
7
7
|
import { type StorageToMarkdownResult } from './to-markdown.js';
|
|
8
|
+
import type { StorageLinkResolver } from './links.js';
|
|
8
9
|
export interface ExportPageOptions {
|
|
10
|
+
linkResolver?: StorageLinkResolver;
|
|
9
11
|
/**
|
|
10
12
|
* Точный путь до итогового md-файла. Альтернатива `outDir`. Аттачи (если
|
|
11
13
|
* включены) кладутся в `attachments/` рядом с этим файлом.
|
|
@@ -16,6 +16,7 @@ export async function exportPage(pageId, opts, cfg) {
|
|
|
16
16
|
mode: opts.mode,
|
|
17
17
|
attachments: opts.attachments,
|
|
18
18
|
tables: opts.tables,
|
|
19
|
+
linkResolver: opts.linkResolver,
|
|
19
20
|
});
|
|
20
21
|
const markdownPath = opts.outFile ?? join(opts.outDir ?? `./${pageId}`, 'page.md');
|
|
21
22
|
mkdirSync(dirname(markdownPath), { recursive: true });
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface StorageLink {
|
|
2
|
+
href?: string;
|
|
3
|
+
pageId?: string;
|
|
4
|
+
title?: string;
|
|
5
|
+
space?: string;
|
|
6
|
+
anchor?: string;
|
|
7
|
+
}
|
|
8
|
+
export type StorageLinkResolver = (link: StorageLink) => string | null | undefined;
|
|
9
|
+
/** Collect targets before a batch determines their final file paths. */
|
|
10
|
+
export declare function storageLinks(storage: string): StorageLink[];
|
|
11
|
+
/** Rewrite links only after the caller has prepared every destination. */
|
|
12
|
+
export declare function rewriteStorageLinks(storage: string, resolver: StorageLinkResolver): string;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { escapeXmlAttr } from '../macros/xml.js';
|
|
2
|
+
import { elements, getAttr, parseStorage, serializeStorage, textContent } from './xhtml.js';
|
|
3
|
+
function visit(nodes, fn) {
|
|
4
|
+
for (const el of nodes) {
|
|
5
|
+
if (el.kind !== 'el' || ['code', 'pre', 'ac:parameter', 'ac:plain-text-body'].includes(el.name))
|
|
6
|
+
continue;
|
|
7
|
+
if (el.name === 'a' && getAttr(el, 'href'))
|
|
8
|
+
fn(el, { href: getAttr(el, 'href') });
|
|
9
|
+
else if (el.name === 'ac:link') {
|
|
10
|
+
const page = elements(el.children).find(c => c.name === 'ri:page');
|
|
11
|
+
if (page)
|
|
12
|
+
fn(el, { pageId: getAttr(page, 'ri:content-id'), title: getAttr(page, 'ri:content-title'), space: getAttr(page, 'ri:space-key'), anchor: getAttr(el, 'ac:anchor') });
|
|
13
|
+
}
|
|
14
|
+
visit(el.children, fn);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Collect targets before a batch determines their final file paths. */
|
|
18
|
+
export function storageLinks(storage) {
|
|
19
|
+
const result = [];
|
|
20
|
+
visit(parseStorage(storage), (_el, link) => result.push(link));
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
/** Rewrite links only after the caller has prepared every destination. */
|
|
24
|
+
export function rewriteStorageLinks(storage, resolver) {
|
|
25
|
+
const nodes = parseStorage(storage);
|
|
26
|
+
let changed = false;
|
|
27
|
+
visit(nodes, (el, link) => {
|
|
28
|
+
const href = resolver(link);
|
|
29
|
+
if (!href || href === link.href)
|
|
30
|
+
return;
|
|
31
|
+
if (el.name === 'ac:link') {
|
|
32
|
+
const body = elements(el.children).find(c => c.name === 'ac:link-body' || c.name === 'ac:plain-text-link-body');
|
|
33
|
+
el.children = body?.name === 'ac:link-body' ? body.children : [{ kind: 'text', raw: escapeXmlAttr(body ? textContent(body.children) : link.title ?? href) }];
|
|
34
|
+
el.name = 'a';
|
|
35
|
+
el.attrs = [];
|
|
36
|
+
el.selfClosing = false;
|
|
37
|
+
}
|
|
38
|
+
el.attrs = [...el.attrs.filter(([key]) => key !== 'href'), ['href', escapeXmlAttr(href)]];
|
|
39
|
+
changed = true;
|
|
40
|
+
});
|
|
41
|
+
return changed ? serializeStorage(nodes) : storage;
|
|
42
|
+
}
|
|
@@ -20,7 +20,10 @@
|
|
|
20
20
|
* выдаётся никогда.
|
|
21
21
|
*/
|
|
22
22
|
import { type MacroRegistry } from '../macros/registry.js';
|
|
23
|
+
import { type StorageLinkResolver } from './links.js';
|
|
23
24
|
export interface StorageToMarkdownOptions {
|
|
25
|
+
/** Map known page references to repository URLs after allocating all batch paths. */
|
|
26
|
+
linkResolver?: StorageLinkResolver;
|
|
24
27
|
/** Known XML attachments; needed for image-mode diagrams without a .drawio suffix. */
|
|
25
28
|
drawioNames?: string[];
|
|
26
29
|
/** Реестр для проверки маркеров макросов (default: встроенный). */
|
|
@@ -26,9 +26,11 @@ import { defaultMacroRegistry } from '../macros/index.js';
|
|
|
26
26
|
import { renderToStorage } from '../markdown/render.js';
|
|
27
27
|
import { compareStorage } from './canonical.js';
|
|
28
28
|
import { NOTICE_MACRO_ID } from '../publish/notice.js';
|
|
29
|
+
import { rewriteStorageLinks } from './links.js';
|
|
29
30
|
import { decodeEntities, elements, getAttr, hasNamespacedElements, parseStorage, serializeStorage, textContent, } from './xhtml.js';
|
|
30
31
|
/** Конвертирует storage-фрагмент страницы в Markdown. */
|
|
31
32
|
export function storageToMarkdown(storage, opts = {}) {
|
|
33
|
+
const rewritten = opts.linkResolver ? rewriteStorageLinks(storage, opts.linkResolver) : storage;
|
|
32
34
|
const local = opts.attachments === 'local';
|
|
33
35
|
const records = opts.tables === 'records';
|
|
34
36
|
// 'local' и 'records' — заведомо не round-trip, поэтому включают readable-
|
|
@@ -41,7 +43,9 @@ export function storageToMarkdown(storage, opts = {}) {
|
|
|
41
43
|
localPrefix: opts.localPrefix ?? 'attachments/',
|
|
42
44
|
drawioNames: opts.drawioNames ?? [],
|
|
43
45
|
});
|
|
44
|
-
const markdown = conv.blocksToMd(parseStorage(
|
|
46
|
+
const markdown = conv.blocksToMd(parseStorage(rewritten));
|
|
47
|
+
if (rewritten !== storage)
|
|
48
|
+
conv.stats.normalized++;
|
|
45
49
|
const attachmentRefs = new Set([...conv.images, ...conv.files]);
|
|
46
50
|
for (const m of storage.matchAll(/ri:filename="([^"]*)"/g))
|
|
47
51
|
attachmentRefs.add(m[1]);
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export { publishPage, computeContentHash, DEFAULT_HASH_PROPERTY_KEY, type Publis
|
|
|
15
15
|
export { buildManagedNotice, applyManagedNotice, DEFAULT_MANAGED_NOTICE_TEXT, DEFAULT_MANAGED_NOTICE_LINK_TEXT, type ManagedNoticeOptions, } from './publish/notice.js';
|
|
16
16
|
export { isHttpUrl, remoteFilename, isSameConfluenceOrigin, remoteRequestHeaders, downloadToFile, } from './publish/remote.js';
|
|
17
17
|
export { runPublish, type Here, type Build, type PublishPlan, type RunPublishOptions, } from './publish/runner.js';
|
|
18
|
+
export { storageLinks, rewriteStorageLinks, type StorageLink, type StorageLinkResolver } from './export/links.js';
|
|
18
19
|
export { parseStorage, serializeStorage, decodeEntities, StorageParseError, type XNode, type XElement, } from './export/xhtml.js';
|
|
19
20
|
export { canonicalize, compareStorage, type CNode, type CompareResult, type StorageDiff, } from './export/canonical.js';
|
|
20
21
|
export { storageToMarkdown, type StorageToMarkdownOptions, type StorageToMarkdownResult, } from './export/to-markdown.js';
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,7 @@ export { buildManagedNotice, applyManagedNotice, DEFAULT_MANAGED_NOTICE_TEXT, DE
|
|
|
23
23
|
export { isHttpUrl, remoteFilename, isSameConfluenceOrigin, remoteRequestHeaders, downloadToFile, } from './publish/remote.js';
|
|
24
24
|
export { runPublish, } from './publish/runner.js';
|
|
25
25
|
// Export (storage → markdown) & round-trip
|
|
26
|
+
export { storageLinks, rewriteStorageLinks } from './export/links.js';
|
|
26
27
|
export { parseStorage, serializeStorage, decodeEntities, StorageParseError, } from './export/xhtml.js';
|
|
27
28
|
export { canonicalize, compareStorage, } from './export/canonical.js';
|
|
28
29
|
export { storageToMarkdown, } from './export/to-markdown.js';
|
|
@@ -54,6 +54,8 @@ export interface RenderStorageOptions {
|
|
|
54
54
|
linkResolver?: (href: string) => {
|
|
55
55
|
title: string;
|
|
56
56
|
space?: string;
|
|
57
|
+
} | {
|
|
58
|
+
url: string;
|
|
57
59
|
} | null;
|
|
58
60
|
/** Local Markdown assets: attachment on this page, or a canonical repository URL. */
|
|
59
61
|
resourceResolver?: (href: string) => {
|
package/dist/markdown/render.js
CHANGED
|
@@ -137,9 +137,11 @@ export function renderToStorage(markdown, urls, opts = {}) {
|
|
|
137
137
|
if (opts.linkResolver || opts.resourceResolver) {
|
|
138
138
|
html = html.replace(/<a href="([^"]*)"(?:\s+title="[^"]*")?>([\s\S]*?)<\/a>/g, (full, href, inner) => {
|
|
139
139
|
const decoded = unescapeHtml(href);
|
|
140
|
-
if (/^(?:
|
|
141
|
-
return full;
|
|
140
|
+
if (/^(?:#|(?:mailto|tel|javascript|data):)/i.test(decoded))
|
|
141
|
+
return full;
|
|
142
142
|
const target = opts.linkResolver?.(decoded);
|
|
143
|
+
if (target && 'url' in target)
|
|
144
|
+
return `<a href="${escapeXmlAttr(target.url)}">${inner}</a>`;
|
|
143
145
|
const body = /<[^>]+>/.test(inner)
|
|
144
146
|
? `<ac:link-body>${inner}</ac:link-body>`
|
|
145
147
|
: plainTextLinkBody(unescapeHtml(inner));
|
|
@@ -148,6 +150,8 @@ export function renderToStorage(markdown, urls, opts = {}) {
|
|
|
148
150
|
const anchor = decoded.includes('#') ? ` ac:anchor="${escapeXmlAttr(decoded.slice(decoded.indexOf('#') + 1))}"` : '';
|
|
149
151
|
return `<ac:link${anchor}><ri:page ri:content-title="${escapeXmlAttr(target.title)}"${space} />${body}</ac:link>`;
|
|
150
152
|
}
|
|
153
|
+
if (/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(decoded))
|
|
154
|
+
return full;
|
|
151
155
|
const resource = opts.resourceResolver?.(decoded);
|
|
152
156
|
if (!resource)
|
|
153
157
|
return full;
|
package/package.json
CHANGED