confluence-md-sync 0.8.7 → 0.8.9
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/macros/registry.js +4 -2
- package/dist/macros/xml.d.ts +14 -1
- package/dist/macros/xml.js +20 -1
- package/dist/markdown/render.d.ts +12 -0
- package/dist/markdown/render.js +22 -0
- package/package.json +1 -1
package/dist/macros/registry.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Markdown } from '../markdown/markdown.js';
|
|
2
2
|
import { unescapeParamValue } from './builder.js';
|
|
3
|
-
import {
|
|
3
|
+
import { stableMacroId, structuredMacro } from './xml.js';
|
|
4
4
|
/**
|
|
5
5
|
* Реестр макросов. Пустой при создании — наполняется плагинами через
|
|
6
6
|
* {@link use} или точечной регистрацией через {@link register}.
|
|
@@ -126,7 +126,9 @@ function processMacroType(storage, macroName, renderer) {
|
|
|
126
126
|
}
|
|
127
127
|
const bodyStart = startIdx + match[0].length;
|
|
128
128
|
const body = storage.substring(bodyStart, endIdx).trim();
|
|
129
|
-
|
|
129
|
+
// Стабильный id от содержимого маркера — иначе он менялся бы на КАЖДЫЙ
|
|
130
|
+
// рендер (см. stableMacroId), ломая content-hash идемпотентность publish.
|
|
131
|
+
const macroId = stableMacroId(`${macroName}\u0000${paramStr}\u0000${body}`);
|
|
130
132
|
const macroXhtml = renderer({ params, body, macroId });
|
|
131
133
|
const before = storage.substring(0, startIdx);
|
|
132
134
|
const after = storage.substring(endIdx + endMarker.length);
|
package/dist/macros/xml.d.ts
CHANGED
|
@@ -2,8 +2,21 @@
|
|
|
2
2
|
import type { MacroParam } from './types.js';
|
|
3
3
|
/** Экранирует строку для XML-атрибутов и текстовых узлов. */
|
|
4
4
|
export declare function escapeXmlAttr(s: string): string;
|
|
5
|
-
/** Генерирует UUID для ac:macro-id. */
|
|
5
|
+
/** Генерирует случайный UUID для ac:macro-id (когда стабильность не нужна). */
|
|
6
6
|
export declare function generateMacroId(): string;
|
|
7
|
+
/**
|
|
8
|
+
* Детерминированный macro-id (UUID-формы) от seed — как правило, от исходного
|
|
9
|
+
* содержимого маркера (имя+параметры+тело). ВАЖНО для идемпотентности
|
|
10
|
+
* content-hash: если id генерировать случайно на каждый рендер (см.
|
|
11
|
+
* {@link generateMacroId}), storage у страницы с любым макросом отличается
|
|
12
|
+
* при каждой публикации, даже когда исходный markdown не менялся — hash
|
|
13
|
+
* никогда не совпадает, и publish принудительно обновляет страницу заново
|
|
14
|
+
* при каждом прогоне (замечено на проде: 1 изменённый файл → 250+ страниц
|
|
15
|
+
* «обновились», потому что ~250 из них содержат макрос details/properties).
|
|
16
|
+
* Стабильный id по содержимому убирает этот псевдо-дрейф: одинаковый маркер
|
|
17
|
+
* даёт одинаковый storage → hash совпадает → UNCHANGED.
|
|
18
|
+
*/
|
|
19
|
+
export declare function stableMacroId(seed: string): string;
|
|
7
20
|
export interface StructuredMacroOptions {
|
|
8
21
|
/**
|
|
9
22
|
* Macro parameters. `undefined` values are skipped. A key rendered as
|
package/dist/macros/xml.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** XML helpers shared by macro renderers. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
/** Экранирует строку для XML-атрибутов и текстовых узлов. */
|
|
3
4
|
export function escapeXmlAttr(s) {
|
|
4
5
|
return s.replace(/[<>&"']/g, (c) => {
|
|
@@ -12,7 +13,7 @@ export function escapeXmlAttr(s) {
|
|
|
12
13
|
}
|
|
13
14
|
});
|
|
14
15
|
}
|
|
15
|
-
/** Генерирует UUID для ac:macro-id. */
|
|
16
|
+
/** Генерирует случайный UUID для ac:macro-id (когда стабильность не нужна). */
|
|
16
17
|
export function generateMacroId() {
|
|
17
18
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
18
19
|
const r = (Math.random() * 16) | 0;
|
|
@@ -20,6 +21,24 @@ export function generateMacroId() {
|
|
|
20
21
|
return v.toString(16);
|
|
21
22
|
});
|
|
22
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Детерминированный macro-id (UUID-формы) от seed — как правило, от исходного
|
|
26
|
+
* содержимого маркера (имя+параметры+тело). ВАЖНО для идемпотентности
|
|
27
|
+
* content-hash: если id генерировать случайно на каждый рендер (см.
|
|
28
|
+
* {@link generateMacroId}), storage у страницы с любым макросом отличается
|
|
29
|
+
* при каждой публикации, даже когда исходный markdown не менялся — hash
|
|
30
|
+
* никогда не совпадает, и publish принудительно обновляет страницу заново
|
|
31
|
+
* при каждом прогоне (замечено на проде: 1 изменённый файл → 250+ страниц
|
|
32
|
+
* «обновились», потому что ~250 из них содержат макрос details/properties).
|
|
33
|
+
* Стабильный id по содержимому убирает этот псевдо-дрейф: одинаковый маркер
|
|
34
|
+
* даёт одинаковый storage → hash совпадает → UNCHANGED.
|
|
35
|
+
*/
|
|
36
|
+
export function stableMacroId(seed) {
|
|
37
|
+
const hex = createHash('sha256').update(seed, 'utf-8').digest('hex');
|
|
38
|
+
const bytes = hex.slice(0, 32);
|
|
39
|
+
return (`${bytes.slice(0, 8)}-${bytes.slice(8, 12)}-4${bytes.slice(13, 16)}-` +
|
|
40
|
+
`${'89ab'[parseInt(bytes[16], 16) % 4]}${bytes.slice(17, 20)}-${bytes.slice(20, 32)}`);
|
|
41
|
+
}
|
|
23
42
|
/**
|
|
24
43
|
* Assembles an `<ac:structured-macro>` element. Takes care of parameter
|
|
25
44
|
* escaping, rich vs plain bodies and CDATA safety.
|
|
@@ -43,6 +43,18 @@ export interface RenderStorageOptions {
|
|
|
43
43
|
fileStyle?: 'url' | 'attachment';
|
|
44
44
|
/** Автопревращение голых URL в ссылки (default: true). */
|
|
45
45
|
linkify?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Резолвер относительных ссылок на другие md-файлы набора. Для каждой
|
|
48
|
+
* markdown-ссылки `[text](href)` вызывается с исходным href; если вернул
|
|
49
|
+
* цель — ссылка публикуется как Confluence page-link (`<ac:link><ri:page>`)
|
|
50
|
+
* на страницу связанного md (карту file→page строит вызывающий, напр.
|
|
51
|
+
* docs-studio из манифеста). `null`/`undefined` — ссылка остаётся как есть
|
|
52
|
+
* (обычная `<a href>`), пригодна для внешних URL и якорей.
|
|
53
|
+
*/
|
|
54
|
+
linkResolver?: (href: string) => {
|
|
55
|
+
title: string;
|
|
56
|
+
space?: string;
|
|
57
|
+
} | null;
|
|
46
58
|
}
|
|
47
59
|
export declare class MissingAttachmentUrlError extends Error {
|
|
48
60
|
constructor(message: string);
|
package/dist/markdown/render.js
CHANGED
|
@@ -116,6 +116,28 @@ export function renderToStorage(markdown, urls, opts = {}) {
|
|
|
116
116
|
// markdown-it сквозь себя не пропускает). Fence рендерится в экранированный
|
|
117
117
|
// <pre><code>; здесь разэкранируем содержимое обратно в живую разметку.
|
|
118
118
|
html = html.replace(/<pre><code class="language-confluence-storage">([\s\S]*?)<\/code><\/pre>\n?/g, (_full, escaped) => unescapeHtml(escaped.replace(/\n$/, '')) + '\n');
|
|
119
|
+
// Ссылки на связанные md → Confluence page-link по резолверу (карта из
|
|
120
|
+
// манифеста). Только markdown-ссылки (`<a href>` из markdown-it); плейсхолдеры
|
|
121
|
+
// {{file:}}/{{img:}} рендерятся ниже и сюда ещё не попали. Внешние URL/якоря
|
|
122
|
+
// резолвер отсекает (возвращает null) — они остаются как есть.
|
|
123
|
+
if (opts.linkResolver) {
|
|
124
|
+
const resolve = opts.linkResolver;
|
|
125
|
+
html = html.replace(/<a href="([^"]*)">([\s\S]*?)<\/a>/g, (full, href, inner) => {
|
|
126
|
+
const decoded = unescapeHtml(href);
|
|
127
|
+
if (/^(?:[a-z][a-z0-9+.-]*:|#|\/\/)/i.test(decoded))
|
|
128
|
+
return full; // http(s):, mailto:, #anchor
|
|
129
|
+
const target = resolve(decoded);
|
|
130
|
+
if (!target)
|
|
131
|
+
return full;
|
|
132
|
+
const text = unescapeHtml(inner.replace(/<[^>]+>/g, '')).trim();
|
|
133
|
+
const attrs = [];
|
|
134
|
+
if (target.space)
|
|
135
|
+
attrs.push(['space', target.space]);
|
|
136
|
+
if (text)
|
|
137
|
+
attrs.push(['text', text]);
|
|
138
|
+
return renderPageLink(target.title, attrs);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
119
141
|
return html.replace(PLACEHOLDER_RE, (_full, kind, rawBody) => {
|
|
120
142
|
const { name, attrs } = parsePlaceholder(String(rawBody));
|
|
121
143
|
if (kind === 'page') {
|
package/package.json
CHANGED