confluence-md-sync 0.6.0 → 0.8.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 +56 -0
- package/dist/export/to-markdown.d.ts +2 -0
- package/dist/export/to-markdown.js +140 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/markdown/native.d.ts +47 -0
- package/dist/markdown/native.js +232 -0
- package/dist/markdown/render.js +4 -0
- package/dist/publish/notice.d.ts +34 -0
- package/dist/publish/notice.js +37 -0
- package/dist/publish/publish.d.ts +8 -0
- package/dist/publish/publish.js +12 -0
- package/dist/publish/runner.d.ts +7 -0
- package/dist/publish/runner.js +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,9 @@ npm install -g confluence-md-sync # as a CLI: `confluence-md-sync …`
|
|
|
25
25
|
- **Attachment dedup** — uploads are tagged `sha256:<hash>`; unchanged files
|
|
26
26
|
are reused. `<file>.src-sha256` sidecars pin dedup to the *source* of
|
|
27
27
|
non-deterministic artifacts (e.g. PNGs rendered by a headless browser).
|
|
28
|
+
- **Managed-page banner** — optionally stamp a “edit this in <source>” notice
|
|
29
|
+
(info/warning panel with a link) into the page; injected into storage only,
|
|
30
|
+
never into the markdown source. See *Managed-page banner*.
|
|
28
31
|
- **Fail before write** — placeholders, files and macro markers are validated
|
|
29
32
|
up front; Confluence is never touched on a broken input.
|
|
30
33
|
- **Pluggable macros** — built-in `core` + `table-filter` plugins, extend
|
|
@@ -121,6 +124,34 @@ await publishPage({
|
|
|
121
124
|
}, cfg);
|
|
122
125
|
```
|
|
123
126
|
|
|
127
|
+
### Managed-page banner
|
|
128
|
+
|
|
129
|
+
When a page is generated from an external source (e.g. docs-studio) and users
|
|
130
|
+
should not edit it in Confluence, add a `managedNotice`. It is injected into the
|
|
131
|
+
**storage** at publish time — never into the markdown source, so export and
|
|
132
|
+
round-trip stay clean, and git remains the single source of truth. The banner is
|
|
133
|
+
deterministic (fixed macro id), so it doesn't break the no-history-spam hash skip.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
await publishPage({
|
|
137
|
+
pageId,
|
|
138
|
+
markdownPath,
|
|
139
|
+
managedNotice: {
|
|
140
|
+
linkUrl: 'https://studio.example/doc/123', // required
|
|
141
|
+
// everything below is optional:
|
|
142
|
+
linkText: 'постановка в docs-studio', // default: 'docs-studio'
|
|
143
|
+
text: 'Правьте страницу в {link}. Здесь только чтение — правки будут перезаписаны.',
|
|
144
|
+
panel: 'warning', // info | note | warning | tip (default: info)
|
|
145
|
+
position: 'top', // 'top' (default) | 'bottom'
|
|
146
|
+
},
|
|
147
|
+
}, cfg);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`{link}` in `text` is replaced by the link; omit it and the link is appended.
|
|
151
|
+
The presence of `managedNotice` turns the banner on — leave it out and no notice
|
|
152
|
+
is added. To apply one banner to every page of a CI plan, pass `managedNotice`
|
|
153
|
+
to `runPublish(dir, plan, { managedNotice })`; a per-page value overrides it.
|
|
154
|
+
|
|
124
155
|
## BPMN diagrams out of the box
|
|
125
156
|
|
|
126
157
|
Pass a `.bpmn` file as an image (a path or an `http(s)` URL) — it is
|
|
@@ -287,6 +318,31 @@ const rows = readCsv(decodeText(await page.getAttachment('data.csv')));
|
|
|
287
318
|
|
|
288
319
|
## Macros
|
|
289
320
|
|
|
321
|
+
### Native Markdown syntax (0.8.0+)
|
|
322
|
+
|
|
323
|
+
Popular macros are written (and exported back) as plain Markdown — no comment
|
|
324
|
+
markers, no raw XHTML. Round-trip is verified canonically per macro; anything
|
|
325
|
+
that doesn't fit falls back to markers/fenced blocks automatically.
|
|
326
|
+
|
|
327
|
+
| Macro | Markdown |
|
|
328
|
+
| --- | --- |
|
|
329
|
+
| `info` / `note` / `warning` / `tip` | `> [!INFO] Title?` + quoted body (GitHub-style admonition) |
|
|
330
|
+
| `details` (Page Properties) | `::: properties [id=…] [hidden=true]` + md-table + `:::` |
|
|
331
|
+
| `expand` | `::: expand Title` + body + `:::` |
|
|
332
|
+
| `panel` | `::: panel title=… borderColor=…` + body + `:::` |
|
|
333
|
+
| `toc` | `{{toc}}` / `{{toc:maxLevel=3}}` |
|
|
334
|
+
| `children` | `{{children}}` / `{{children:depth=2}}` |
|
|
335
|
+
| `jira` | `{{jira:KEY-1}}` / `{{jira:jql=project = X\|maximumIssues=20}}` |
|
|
336
|
+
| `status` | `{{status:Текст\|colour=Green\|subtle=true}}` |
|
|
337
|
+
| `anchor` | `{{anchor:name}}` |
|
|
338
|
+
| `detailssummary` (Page Properties Report) | `{{properties-report:cql=…\|firstcolumn=…}}` |
|
|
339
|
+
|
|
340
|
+
Inside fenced code blocks the syntax is left untouched. The machine-readable
|
|
341
|
+
list is exported as `nativeMacroList()`; the md→markers pass is
|
|
342
|
+
`nativeToMarkers()` (runs automatically inside `renderToStorage`).
|
|
343
|
+
|
|
344
|
+
### Marker builders
|
|
345
|
+
|
|
290
346
|
Builders return `Markdown` with comment markers; markers become
|
|
291
347
|
`<ac:structured-macro>` after rendering (nested macros resolve inner-first):
|
|
292
348
|
|
|
@@ -61,6 +61,8 @@ export interface StorageToMarkdownResult {
|
|
|
61
61
|
rawHtml: number;
|
|
62
62
|
/** readable-режим: сколько узлов конвертировано с потерей оформления. */
|
|
63
63
|
lossy: number;
|
|
64
|
+
/** Макросы, выраженные нативным md-синтаксисом (панели, ::: …, {{…}}). */
|
|
65
|
+
native: number;
|
|
64
66
|
};
|
|
65
67
|
}
|
|
66
68
|
/** Конвертирует storage-фрагмент страницы в Markdown. */
|
|
@@ -79,7 +79,7 @@ class Converter {
|
|
|
79
79
|
registry;
|
|
80
80
|
images = new Set();
|
|
81
81
|
files = new Set();
|
|
82
|
-
stats = { markers: 0, fenced: 0, rawHtml: 0, lossy: 0 };
|
|
82
|
+
stats = { markers: 0, fenced: 0, rawHtml: 0, lossy: 0, native: 0 };
|
|
83
83
|
readable;
|
|
84
84
|
localFiles;
|
|
85
85
|
tablesAsRecords;
|
|
@@ -290,8 +290,147 @@ class Converter {
|
|
|
290
290
|
return escapeMdText(textContent(el.children), {}, this.readable).trim();
|
|
291
291
|
}
|
|
292
292
|
// ── Макросы ──────────────────────────────────────────────────────────
|
|
293
|
+
// ── Нативный md-синтаксис (native.ts): панели, ::: properties/expand/panel,
|
|
294
|
+
// {{toc}}/{{children}}/{{jira}}/{{status}}/{{anchor}}/{{properties-report}}.
|
|
295
|
+
// Каждый кандидат проверяется рендером и канонической сверкой (как маркеры).
|
|
296
|
+
static NATIVE_ADMONITION_TAG = {
|
|
297
|
+
info: 'INFO', note: 'NOTE', warning: 'WARNING', tip: 'TIP',
|
|
298
|
+
};
|
|
299
|
+
static NATIVE_PLACEHOLDER_NAME = {
|
|
300
|
+
toc: 'toc', children: 'children', jira: 'jira', status: 'status',
|
|
301
|
+
anchor: 'anchor', detailssummary: 'properties-report',
|
|
302
|
+
};
|
|
303
|
+
tryNativeMd(el, name) {
|
|
304
|
+
if (this.readable)
|
|
305
|
+
return null; // readable-путь остаётся прежним
|
|
306
|
+
const parts = this.macroParts(el);
|
|
307
|
+
if (parts === null)
|
|
308
|
+
return null;
|
|
309
|
+
const { params, richBody, plainBody } = parts;
|
|
310
|
+
if (plainBody !== null)
|
|
311
|
+
return null;
|
|
312
|
+
const pmap = new Map(params.map((p) => [p.name, p.value]));
|
|
313
|
+
const oneline = (v) => v !== undefined && !/[\n|{}]/.test(v) && !v.includes('}}');
|
|
314
|
+
let candidate = null;
|
|
315
|
+
const tag = Converter.NATIVE_ADMONITION_TAG[name];
|
|
316
|
+
if (tag && richBody !== null) {
|
|
317
|
+
// Панель: параметры — только title.
|
|
318
|
+
if (![...pmap.keys()].every((k) => k === 'title'))
|
|
319
|
+
return null;
|
|
320
|
+
const title = pmap.get('title');
|
|
321
|
+
if (title !== undefined && (!oneline(title) || /\[|\]/.test(title)))
|
|
322
|
+
return null;
|
|
323
|
+
const bodyMd = this.bodyMd(richBody);
|
|
324
|
+
if (bodyMd === null || bodyMd.trim() === '')
|
|
325
|
+
return null;
|
|
326
|
+
const quoted = bodyMd.split('\n').map((l) => (l === '' ? '>' : `> ${l}`)).join('\n');
|
|
327
|
+
candidate = `> [!${tag}]${title ? ' ' + title : ''}\n${quoted}`;
|
|
328
|
+
}
|
|
329
|
+
else if (name === 'details' && richBody !== null) {
|
|
330
|
+
// «Свойства страницы»: параметры id/hidden, тело — простая md-таблица.
|
|
331
|
+
if (![...pmap.keys()].every((k) => k === 'id' || k === 'hidden'))
|
|
332
|
+
return null;
|
|
333
|
+
const bodyEls = elements(richBody.children);
|
|
334
|
+
const nonWs = richBody.children.filter((n) => !(n.kind === 'text' && /^[ \t\r\n]*$/.test(n.raw)));
|
|
335
|
+
if (bodyEls.length !== 1 || nonWs.length !== 1 || bodyEls[0].name !== 'table')
|
|
336
|
+
return null;
|
|
337
|
+
let tableMd;
|
|
338
|
+
try {
|
|
339
|
+
tableMd = this.tableToMd(bodyEls[0]);
|
|
340
|
+
}
|
|
341
|
+
catch (e) {
|
|
342
|
+
if (e instanceof Unrepresentable)
|
|
343
|
+
return null;
|
|
344
|
+
throw e;
|
|
345
|
+
}
|
|
346
|
+
const dp = [...pmap.entries()].map(([k, v]) => (oneline(v) && !/["\s]/.test(v) ? ` ${k}=${v}` : null));
|
|
347
|
+
if (dp.some((x) => x === null))
|
|
348
|
+
return null;
|
|
349
|
+
candidate = `::: properties${dp.join('')}\n${tableMd}\n:::`;
|
|
350
|
+
}
|
|
351
|
+
else if (name === 'expand' && richBody !== null) {
|
|
352
|
+
if (![...pmap.keys()].every((k) => k === 'title'))
|
|
353
|
+
return null;
|
|
354
|
+
const title = pmap.get('title');
|
|
355
|
+
if (title !== undefined && (!oneline(title) || /=|"/.test(title)))
|
|
356
|
+
return null;
|
|
357
|
+
const bodyMd = this.bodyMd(richBody);
|
|
358
|
+
if (bodyMd === null)
|
|
359
|
+
return null;
|
|
360
|
+
candidate = `::: expand${title ? ' ' + title : ''}\n${bodyMd}\n:::`;
|
|
361
|
+
}
|
|
362
|
+
else if (Converter.NATIVE_PLACEHOLDER_NAME[name] && richBody === null) {
|
|
363
|
+
// Bodyless-плейсхолдеры. Части значений с | или }} не выразить — маркер.
|
|
364
|
+
const ph = Converter.NATIVE_PLACEHOLDER_NAME[name];
|
|
365
|
+
const attrs = [];
|
|
366
|
+
let head = '';
|
|
367
|
+
for (const p of params) {
|
|
368
|
+
if (!oneline(p.value) || p.value.includes('|'))
|
|
369
|
+
return null;
|
|
370
|
+
if (name === 'jira' && p.name === 'key' && head === '') {
|
|
371
|
+
head = p.value;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (name === 'status' && p.name === 'title' && head === '') {
|
|
375
|
+
head = p.value;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (name === 'anchor' && (p.name === 'name' || p.name === '') && head === '') {
|
|
379
|
+
head = p.value;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const attrName = name === 'jira' && p.name === 'jqlQuery' ? 'jql' : p.name;
|
|
383
|
+
if (!/^[A-Za-z-]+$/.test(attrName))
|
|
384
|
+
return null;
|
|
385
|
+
attrs.push(`${attrName}=${p.value}`);
|
|
386
|
+
}
|
|
387
|
+
const inner = [head, ...attrs].filter((s, i) => s !== '' || i > 0).join('|');
|
|
388
|
+
candidate = inner === '' ? `{{${ph}}}` : `{{${ph}:${inner}}}`;
|
|
389
|
+
}
|
|
390
|
+
if (candidate === null)
|
|
391
|
+
return null;
|
|
392
|
+
return this.verifyMacroMarker(el, candidate) ? candidate : null;
|
|
393
|
+
}
|
|
394
|
+
/** Разбирает macro-элемент на параметры и тела; null — посторонние дети. */
|
|
395
|
+
macroParts(el) {
|
|
396
|
+
const params = [];
|
|
397
|
+
let richBody = null;
|
|
398
|
+
let plainBody = null;
|
|
399
|
+
for (const child of elements(el.children)) {
|
|
400
|
+
if (child.name === 'ac:parameter') {
|
|
401
|
+
const pname = getAttr(child, 'ac:name') ?? '';
|
|
402
|
+
params.push({ name: pname, value: decodeEntities(textContent(child.children)) });
|
|
403
|
+
}
|
|
404
|
+
else if (child.name === 'ac:rich-text-body')
|
|
405
|
+
richBody = child;
|
|
406
|
+
else if (child.name === 'ac:plain-text-body')
|
|
407
|
+
plainBody = child;
|
|
408
|
+
else
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
return { params, richBody, plainBody };
|
|
412
|
+
}
|
|
413
|
+
/** Тело макроса → md; null, если не легло без потерь. */
|
|
414
|
+
bodyMd(richBody) {
|
|
415
|
+
const before = { images: new Set(this.images), files: new Set(this.files) };
|
|
416
|
+
try {
|
|
417
|
+
return this.blocksToMd(richBody.children).trimEnd();
|
|
418
|
+
}
|
|
419
|
+
catch (e) {
|
|
420
|
+
if (!(e instanceof Unrepresentable))
|
|
421
|
+
throw e;
|
|
422
|
+
this.images = before.images;
|
|
423
|
+
this.files = before.files;
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
293
427
|
macroToMd(el) {
|
|
294
428
|
const name = getAttr(el, 'ac:name') ?? '';
|
|
429
|
+
const nativeMd = this.tryNativeMd(el, name);
|
|
430
|
+
if (nativeMd !== null) {
|
|
431
|
+
this.stats.native++;
|
|
432
|
+
return nativeMd;
|
|
433
|
+
}
|
|
295
434
|
const markerMd = this.tryMacroMarker(el, name);
|
|
296
435
|
if (markerMd !== null) {
|
|
297
436
|
this.stats.markers++;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { authHeader, loadConfigFromEnv, type ConfluenceAuthType, type Confluence
|
|
|
3
3
|
export { Markdown } from './markdown/markdown.js';
|
|
4
4
|
export { renderToStorage, extractPlaceholders, parsePlaceholder, renameImagePlaceholders, MissingAttachmentUrlError, PLACEHOLDER_RE, type AttachmentUrls, type ExtractedPlaceholders, type PlaceholderRef, type RenderStorageOptions, } from './markdown/render.js';
|
|
5
5
|
export { validateMarkdown, MarkdownValidationError, type ValidateOptions } from './markdown/validate.js';
|
|
6
|
+
export { nativeToMarkers, nativeMacroList, NATIVE_ADMONITIONS, NATIVE_DIRECTIVES, NATIVE_PLACEHOLDERS, } from './markdown/native.js';
|
|
6
7
|
export * from './macros/index.js';
|
|
7
8
|
export { convertBpmn, convertBpmnFolder, isBpmnFile, bpmnOutputName, BPMN_FILE_RE, type BpmnConversion, type BpmnImageFormat, type ConvertBpmnFolderOptions, } from './bpmn/convert.js';
|
|
8
9
|
export { fileSha256, HASH_TAG_PREFIX } from './attachments/hash.js';
|
|
@@ -10,6 +11,7 @@ export { Attachment, AttachmentService, toAttachmentVersion, SRC_SHA_SIDECAR_SUF
|
|
|
10
11
|
export { Page, Table } from './pages/page.js';
|
|
11
12
|
export { readTableFromConfluence, findTable, findTableInMacro, parseHtmlTable, decodeHtmlCell, renderMarkdownTable, escapeMdTableCell, readAndMapTable, type ColumnAlign, type TableColumn, } from './pages/tables.js';
|
|
12
13
|
export { publishPage, computeContentHash, DEFAULT_HASH_PROPERTY_KEY, type PublishPageOptions, type PublishPageResult, type TableData, } from './publish/publish.js';
|
|
14
|
+
export { buildManagedNotice, applyManagedNotice, DEFAULT_MANAGED_NOTICE_TEXT, DEFAULT_MANAGED_NOTICE_LINK_TEXT, type ManagedNoticeOptions, } from './publish/notice.js';
|
|
13
15
|
export { isHttpUrl, remoteFilename, isSameConfluenceOrigin, remoteRequestHeaders, downloadToFile, } from './publish/remote.js';
|
|
14
16
|
export { runPublish, type Here, type Build, type PublishPlan, type RunPublishOptions, } from './publish/runner.js';
|
|
15
17
|
export { parseStorage, serializeStorage, decodeEntities, StorageParseError, type XNode, type XElement, } from './export/xhtml.js';
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export { authHeader, loadConfigFromEnv, } from './client/config.js';
|
|
|
5
5
|
export { Markdown } from './markdown/markdown.js';
|
|
6
6
|
export { renderToStorage, extractPlaceholders, parsePlaceholder, renameImagePlaceholders, MissingAttachmentUrlError, PLACEHOLDER_RE, } from './markdown/render.js';
|
|
7
7
|
export { validateMarkdown, MarkdownValidationError } from './markdown/validate.js';
|
|
8
|
+
export { nativeToMarkers, nativeMacroList, NATIVE_ADMONITIONS, NATIVE_DIRECTIVES, NATIVE_PLACEHOLDERS, } from './markdown/native.js';
|
|
8
9
|
// Macros (pluggable)
|
|
9
10
|
export * from './macros/index.js';
|
|
10
11
|
// BPMN (optional peer dep 'bpmn-to-image' is loaded lazily on use)
|
|
@@ -17,6 +18,7 @@ export { Page, Table } from './pages/page.js';
|
|
|
17
18
|
export { readTableFromConfluence, findTable, findTableInMacro, parseHtmlTable, decodeHtmlCell, renderMarkdownTable, escapeMdTableCell, readAndMapTable, } from './pages/tables.js';
|
|
18
19
|
// Publish
|
|
19
20
|
export { publishPage, computeContentHash, DEFAULT_HASH_PROPERTY_KEY, } from './publish/publish.js';
|
|
21
|
+
export { buildManagedNotice, applyManagedNotice, DEFAULT_MANAGED_NOTICE_TEXT, DEFAULT_MANAGED_NOTICE_LINK_TEXT, } from './publish/notice.js';
|
|
20
22
|
export { isHttpUrl, remoteFilename, isSameConfluenceOrigin, remoteRequestHeaders, downloadToFile, } from './publish/remote.js';
|
|
21
23
|
export { runPublish, } from './publish/runner.js';
|
|
22
24
|
// Export (storage → markdown) & round-trip
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Нативный Markdown-синтаксис для популярных макросов Confluence.
|
|
3
|
+
* Сахар над маркерами <!-- MACRO:start/end -->: перед рендером native-
|
|
4
|
+
* конструкции переписываются в маркеры (nativeToMarkers), а экспорт
|
|
5
|
+
* (storage → md) эмитит их обратно с канонической верификацией.
|
|
6
|
+
*
|
|
7
|
+
* Перечень (см. README, раздел «Нативные макросы»):
|
|
8
|
+
*
|
|
9
|
+
* Панели (admonition в стиле GitHub):
|
|
10
|
+
* > [!INFO] Заголовок → info (заголовок опционален)
|
|
11
|
+
* > [!NOTE] / [!WARNING] / [!TIP] → note / warning / tip
|
|
12
|
+
* > текст панели — обычные строки цитаты после тега.
|
|
13
|
+
*
|
|
14
|
+
* Блоки-директивы (fenced ::: … :::):
|
|
15
|
+
* ::: properties [id=x] [hidden=true] → details («Свойства страницы»;
|
|
16
|
+
* тело — обычная md-таблица) работает с отчётом detailssummary
|
|
17
|
+
* ::: expand Заголовок → expand (разворачиваемый блок)
|
|
18
|
+
* ::: panel title=… borderColor=… → panel
|
|
19
|
+
*
|
|
20
|
+
* Строчные плейсхолдеры (семейство {{img:}}/{{page:}}):
|
|
21
|
+
* {{toc}} / {{toc:maxLevel=3}} → toc (оглавление)
|
|
22
|
+
* {{children}} / {{children:depth=2}} → children (дочерние страницы)
|
|
23
|
+
* {{jira:DR-123}} → jira (карточка задачи)
|
|
24
|
+
* {{jira:jql=project = DR|maximumIssues=20}} → jira (выгрузка по JQL)
|
|
25
|
+
* {{status:Готово|colour=Green}} → status (лейбл)
|
|
26
|
+
* {{anchor:имя}} → anchor (якорь)
|
|
27
|
+
* {{properties-report:cql=label = "x"}} → detailssummary (отчёт по
|
|
28
|
+
* свойствам страниц)
|
|
29
|
+
*
|
|
30
|
+
* Внутри fenced-код-блоков (``` / ~~~) синтаксис не интерпретируется.
|
|
31
|
+
*/
|
|
32
|
+
/** Панели: тег admonition → имя макроса. */
|
|
33
|
+
export declare const NATIVE_ADMONITIONS: Record<string, string>;
|
|
34
|
+
/** Блоки-директивы: имя директивы → имя макроса. */
|
|
35
|
+
export declare const NATIVE_DIRECTIVES: Record<string, string>;
|
|
36
|
+
/** Строчные плейсхолдеры: имя → имя макроса. */
|
|
37
|
+
export declare const NATIVE_PLACEHOLDERS: Record<string, string>;
|
|
38
|
+
/** Полный перечень макросов с нативной md-разметкой (для документации/UI). */
|
|
39
|
+
export declare function nativeMacroList(): {
|
|
40
|
+
macro: string;
|
|
41
|
+
syntax: string;
|
|
42
|
+
}[];
|
|
43
|
+
/**
|
|
44
|
+
* Переписывает нативные конструкции в маркеры макросов. Идемпотентна для
|
|
45
|
+
* текста без нативного синтаксиса; содержимое fenced-код-блоков не трогает.
|
|
46
|
+
*/
|
|
47
|
+
export declare function nativeToMarkers(src: string): string;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Нативный Markdown-синтаксис для популярных макросов Confluence.
|
|
3
|
+
* Сахар над маркерами <!-- MACRO:start/end -->: перед рендером native-
|
|
4
|
+
* конструкции переписываются в маркеры (nativeToMarkers), а экспорт
|
|
5
|
+
* (storage → md) эмитит их обратно с канонической верификацией.
|
|
6
|
+
*
|
|
7
|
+
* Перечень (см. README, раздел «Нативные макросы»):
|
|
8
|
+
*
|
|
9
|
+
* Панели (admonition в стиле GitHub):
|
|
10
|
+
* > [!INFO] Заголовок → info (заголовок опционален)
|
|
11
|
+
* > [!NOTE] / [!WARNING] / [!TIP] → note / warning / tip
|
|
12
|
+
* > текст панели — обычные строки цитаты после тега.
|
|
13
|
+
*
|
|
14
|
+
* Блоки-директивы (fenced ::: … :::):
|
|
15
|
+
* ::: properties [id=x] [hidden=true] → details («Свойства страницы»;
|
|
16
|
+
* тело — обычная md-таблица) работает с отчётом detailssummary
|
|
17
|
+
* ::: expand Заголовок → expand (разворачиваемый блок)
|
|
18
|
+
* ::: panel title=… borderColor=… → panel
|
|
19
|
+
*
|
|
20
|
+
* Строчные плейсхолдеры (семейство {{img:}}/{{page:}}):
|
|
21
|
+
* {{toc}} / {{toc:maxLevel=3}} → toc (оглавление)
|
|
22
|
+
* {{children}} / {{children:depth=2}} → children (дочерние страницы)
|
|
23
|
+
* {{jira:DR-123}} → jira (карточка задачи)
|
|
24
|
+
* {{jira:jql=project = DR|maximumIssues=20}} → jira (выгрузка по JQL)
|
|
25
|
+
* {{status:Готово|colour=Green}} → status (лейбл)
|
|
26
|
+
* {{anchor:имя}} → anchor (якорь)
|
|
27
|
+
* {{properties-report:cql=label = "x"}} → detailssummary (отчёт по
|
|
28
|
+
* свойствам страниц)
|
|
29
|
+
*
|
|
30
|
+
* Внутри fenced-код-блоков (``` / ~~~) синтаксис не интерпретируется.
|
|
31
|
+
*/
|
|
32
|
+
import { escapeParamValue } from '../macros/builder.js';
|
|
33
|
+
/** Панели: тег admonition → имя макроса. */
|
|
34
|
+
export const NATIVE_ADMONITIONS = {
|
|
35
|
+
INFO: 'info',
|
|
36
|
+
NOTE: 'note',
|
|
37
|
+
WARNING: 'warning',
|
|
38
|
+
TIP: 'tip',
|
|
39
|
+
};
|
|
40
|
+
/** Блоки-директивы: имя директивы → имя макроса. */
|
|
41
|
+
export const NATIVE_DIRECTIVES = {
|
|
42
|
+
properties: 'details',
|
|
43
|
+
expand: 'expand',
|
|
44
|
+
panel: 'panel',
|
|
45
|
+
};
|
|
46
|
+
/** Строчные плейсхолдеры: имя → имя макроса. */
|
|
47
|
+
export const NATIVE_PLACEHOLDERS = {
|
|
48
|
+
toc: 'toc',
|
|
49
|
+
children: 'children',
|
|
50
|
+
jira: 'jira',
|
|
51
|
+
status: 'status',
|
|
52
|
+
anchor: 'anchor',
|
|
53
|
+
'properties-report': 'detailssummary',
|
|
54
|
+
};
|
|
55
|
+
/** Полный перечень макросов с нативной md-разметкой (для документации/UI). */
|
|
56
|
+
export function nativeMacroList() {
|
|
57
|
+
return [
|
|
58
|
+
{ macro: 'info', syntax: '> [!INFO] Заголовок?' },
|
|
59
|
+
{ macro: 'note', syntax: '> [!NOTE] Заголовок?' },
|
|
60
|
+
{ macro: 'warning', syntax: '> [!WARNING] Заголовок?' },
|
|
61
|
+
{ macro: 'tip', syntax: '> [!TIP] Заголовок?' },
|
|
62
|
+
{ macro: 'details', syntax: '::: properties [id=…] [hidden=true] … :::' },
|
|
63
|
+
{ macro: 'expand', syntax: '::: expand Заголовок … :::' },
|
|
64
|
+
{ macro: 'panel', syntax: '::: panel title=… … :::' },
|
|
65
|
+
{ macro: 'toc', syntax: '{{toc}} | {{toc:maxLevel=3}}' },
|
|
66
|
+
{ macro: 'children', syntax: '{{children}} | {{children:depth=2}}' },
|
|
67
|
+
{ macro: 'jira', syntax: '{{jira:KEY-1}} | {{jira:jql=…|maximumIssues=20}}' },
|
|
68
|
+
{ macro: 'status', syntax: '{{status:Текст|colour=Green|subtle=true}}' },
|
|
69
|
+
{ macro: 'anchor', syntax: '{{anchor:имя}}' },
|
|
70
|
+
{ macro: 'detailssummary', syntax: '{{properties-report:cql=…|firstcolumn=…}}' },
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
const ADMONITION_FIRST_RE = /^>\s*\[!([A-Za-z]+)\]\s*(.*)$/;
|
|
74
|
+
const DIRECTIVE_OPEN_RE = /^:::\s+([a-z-]+)(?:\s+(.*?))?\s*$/;
|
|
75
|
+
const DIRECTIVE_CLOSE_RE = /^:::\s*$/;
|
|
76
|
+
const PLACEHOLDER_INLINE_RE = /\{\{(toc|children|jira|status|anchor|properties-report)(?::((?:[^{}]|\{[^{])*?))?\}\}/g;
|
|
77
|
+
const FENCE_RE = /^\s*(`{3,}|~{3,})/;
|
|
78
|
+
/** Компактная пара маркеров без тела — для строчного контекста. */
|
|
79
|
+
function inlineMarker(macroName, params) {
|
|
80
|
+
const paramStr = params.length
|
|
81
|
+
? ':' + params.map((p) => `${escapeParamValue(p.name)}=${escapeParamValue(p.value)}`).join(':')
|
|
82
|
+
: '';
|
|
83
|
+
return `<!-- MACRO:start:${macroName}${paramStr} --><!-- MACRO:end:${macroName} -->`;
|
|
84
|
+
}
|
|
85
|
+
function blockMarker(macroName, params, body) {
|
|
86
|
+
const paramStr = params.length
|
|
87
|
+
? ':' + params.map((p) => `${escapeParamValue(p.name)}=${escapeParamValue(p.value)}`).join(':')
|
|
88
|
+
: '';
|
|
89
|
+
return `<!-- MACRO:start:${macroName}${paramStr} -->\n${body}\n<!-- MACRO:end:${macroName} -->`;
|
|
90
|
+
}
|
|
91
|
+
/** `a|b=c|d=e` → первый сегмент + пары; поведение как у parsePlaceholder. */
|
|
92
|
+
function splitAttrs(raw) {
|
|
93
|
+
const parts = raw.split('|');
|
|
94
|
+
const attrs = [];
|
|
95
|
+
for (const part of parts.slice(1)) {
|
|
96
|
+
const eq = part.indexOf('=');
|
|
97
|
+
if (eq === -1)
|
|
98
|
+
attrs.push({ name: part.trim(), value: '' });
|
|
99
|
+
else
|
|
100
|
+
attrs.push({ name: part.slice(0, eq).trim(), value: part.slice(eq + 1).trim() });
|
|
101
|
+
}
|
|
102
|
+
return { head: parts[0].trim(), attrs };
|
|
103
|
+
}
|
|
104
|
+
/** Параметры плейсхолдера → параметры макроса Confluence. */
|
|
105
|
+
function placeholderParams(kind, raw) {
|
|
106
|
+
if (raw === undefined || raw.trim() === '')
|
|
107
|
+
return [];
|
|
108
|
+
const { head, attrs } = splitAttrs(raw);
|
|
109
|
+
const params = [];
|
|
110
|
+
const headIsPair = head.includes('=');
|
|
111
|
+
if (headIsPair) {
|
|
112
|
+
const eq = head.indexOf('=');
|
|
113
|
+
attrs.unshift({ name: head.slice(0, eq).trim(), value: head.slice(eq + 1).trim() });
|
|
114
|
+
}
|
|
115
|
+
switch (kind) {
|
|
116
|
+
case 'jira':
|
|
117
|
+
if (!headIsPair && head !== '')
|
|
118
|
+
params.push({ name: 'key', value: head });
|
|
119
|
+
break;
|
|
120
|
+
case 'status':
|
|
121
|
+
if (!headIsPair && head !== '')
|
|
122
|
+
params.push({ name: 'title', value: head });
|
|
123
|
+
break;
|
|
124
|
+
case 'anchor':
|
|
125
|
+
if (!headIsPair && head !== '')
|
|
126
|
+
params.push({ name: 'name', value: head });
|
|
127
|
+
break;
|
|
128
|
+
default:
|
|
129
|
+
// toc/children/properties-report: только k=v-атрибуты.
|
|
130
|
+
if (!headIsPair && head !== '')
|
|
131
|
+
params.push({ name: head, value: '' });
|
|
132
|
+
}
|
|
133
|
+
for (const a of attrs) {
|
|
134
|
+
// jira: короткое `jql=` — алиас родного jqlQuery.
|
|
135
|
+
if (kind === 'jira' && a.name === 'jql')
|
|
136
|
+
params.push({ name: 'jqlQuery', value: a.value });
|
|
137
|
+
else
|
|
138
|
+
params.push(a);
|
|
139
|
+
}
|
|
140
|
+
return params;
|
|
141
|
+
}
|
|
142
|
+
function replaceInline(line) {
|
|
143
|
+
return line.replace(PLACEHOLDER_INLINE_RE, (_full, kind, raw) => inlineMarker(NATIVE_PLACEHOLDERS[kind], placeholderParams(kind, raw)));
|
|
144
|
+
}
|
|
145
|
+
/** Параметры директивы: токены `k=v` (значение можно в кавычках) + свободный текст → title. */
|
|
146
|
+
function directiveParams(name, rest) {
|
|
147
|
+
const params = [];
|
|
148
|
+
if (!rest)
|
|
149
|
+
return params;
|
|
150
|
+
const free = [];
|
|
151
|
+
const re = /([A-Za-z-]+)=("([^"]*)"|\S+)|(\S+)/g;
|
|
152
|
+
for (const m of rest.matchAll(re)) {
|
|
153
|
+
if (m[1])
|
|
154
|
+
params.push({ name: m[1], value: m[3] ?? m[2] });
|
|
155
|
+
else
|
|
156
|
+
free.push(m[4]);
|
|
157
|
+
}
|
|
158
|
+
if (free.length && name === 'expand')
|
|
159
|
+
params.unshift({ name: 'title', value: free.join(' ') });
|
|
160
|
+
return params;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Переписывает нативные конструкции в маркеры макросов. Идемпотентна для
|
|
164
|
+
* текста без нативного синтаксиса; содержимое fenced-код-блоков не трогает.
|
|
165
|
+
*/
|
|
166
|
+
export function nativeToMarkers(src) {
|
|
167
|
+
const lines = src.split('\n');
|
|
168
|
+
const out = [];
|
|
169
|
+
let fence = null;
|
|
170
|
+
for (let i = 0; i < lines.length; i++) {
|
|
171
|
+
const line = lines[i];
|
|
172
|
+
const fm = FENCE_RE.exec(line);
|
|
173
|
+
if (fm) {
|
|
174
|
+
if (fence === null)
|
|
175
|
+
fence = fm[1][0];
|
|
176
|
+
else if (fm[1][0] === fence)
|
|
177
|
+
fence = null;
|
|
178
|
+
out.push(line);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (fence !== null) {
|
|
182
|
+
out.push(line);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
// ── Панели: > [!TYPE] Заголовок? ─────────────────────────────────────
|
|
186
|
+
const am = ADMONITION_FIRST_RE.exec(line);
|
|
187
|
+
const macroName = am ? NATIVE_ADMONITIONS[am[1].toUpperCase()] : undefined;
|
|
188
|
+
if (am && macroName) {
|
|
189
|
+
const body = [];
|
|
190
|
+
let j = i + 1;
|
|
191
|
+
for (; j < lines.length && /^>( |$)/.test(lines[j]); j++) {
|
|
192
|
+
body.push(lines[j].replace(/^> ?/, ''));
|
|
193
|
+
}
|
|
194
|
+
const params = am[2].trim() ? [{ name: 'title', value: am[2].trim() }] : [];
|
|
195
|
+
out.push(blockMarker(macroName, params, nativeToMarkers(body.join('\n'))));
|
|
196
|
+
i = j - 1;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
// ── Директивы: ::: name … / ::: ──────────────────────────────────────
|
|
200
|
+
const dm = DIRECTIVE_OPEN_RE.exec(line);
|
|
201
|
+
const dirMacro = dm ? NATIVE_DIRECTIVES[dm[1]] : undefined;
|
|
202
|
+
if (dm && dirMacro) {
|
|
203
|
+
const body = [];
|
|
204
|
+
let j = i + 1;
|
|
205
|
+
let innerFence = null;
|
|
206
|
+
let closed = false;
|
|
207
|
+
for (; j < lines.length; j++) {
|
|
208
|
+
const bl = lines[j];
|
|
209
|
+
const bfm = FENCE_RE.exec(bl);
|
|
210
|
+
if (bfm) {
|
|
211
|
+
if (innerFence === null)
|
|
212
|
+
innerFence = bfm[1][0];
|
|
213
|
+
else if (bfm[1][0] === innerFence)
|
|
214
|
+
innerFence = null;
|
|
215
|
+
}
|
|
216
|
+
else if (innerFence === null && DIRECTIVE_CLOSE_RE.test(bl)) {
|
|
217
|
+
closed = true;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
body.push(bl);
|
|
221
|
+
}
|
|
222
|
+
if (closed) {
|
|
223
|
+
out.push(blockMarker(dirMacro, directiveParams(dm[1], dm[2]), nativeToMarkers(body.join('\n'))));
|
|
224
|
+
i = j;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
// Незакрытая директива — оставляем как текст.
|
|
228
|
+
}
|
|
229
|
+
out.push(replaceInline(line));
|
|
230
|
+
}
|
|
231
|
+
return out.join('\n');
|
|
232
|
+
}
|
package/dist/markdown/render.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import MarkdownIt from 'markdown-it';
|
|
2
|
+
import { nativeToMarkers } from './native.js';
|
|
2
3
|
// xhtmlOut: true — Confluence storage format = XHTML, void-элементы
|
|
3
4
|
// (<hr/>, <br/>, <img/>) обязаны быть самозакрывающимися.
|
|
4
5
|
// html: true — разрешить HTML (нужно для <!-- MACRO:... --> комментариев)
|
|
@@ -106,6 +107,9 @@ export class MissingAttachmentUrlError extends Error {
|
|
|
106
107
|
* посимвольно.
|
|
107
108
|
*/
|
|
108
109
|
export function renderToStorage(markdown, urls, opts = {}) {
|
|
110
|
+
// Нативный md-синтаксис макросов (панели, ::: properties, {{toc}} …) —
|
|
111
|
+
// сахар над маркерами: переписываем до markdown-it (см. native.ts).
|
|
112
|
+
markdown = nativeToMarkers(markdown);
|
|
109
113
|
const renderer = opts.linkify === false ? mdNoLinkify : md;
|
|
110
114
|
let html = renderer.render(markdown);
|
|
111
115
|
// ```confluence-storage — транспорт для дословного XHTML (ac:/ri:-теги
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* «Баннер управляемой страницы»: примечание о том, что страница ведётся во
|
|
3
|
+
* внешнем источнике (docs-studio), а правки в самой Confluence будут
|
|
4
|
+
* перезаписаны. Вставляется в storage при публикации, но НЕ в markdown-
|
|
5
|
+
* источник — round-trip/экспорт его не увидят, git остаётся чистым.
|
|
6
|
+
*/
|
|
7
|
+
export interface ManagedNoticeOptions {
|
|
8
|
+
/**
|
|
9
|
+
* URL источника (docs-studio / репозиторий постановки), куда ведёт ссылка.
|
|
10
|
+
* Обязателен — без него примечание не имеет смысла.
|
|
11
|
+
*/
|
|
12
|
+
linkUrl: string;
|
|
13
|
+
/** Текст ссылки. Default: {@link DEFAULT_MANAGED_NOTICE_LINK_TEXT}. */
|
|
14
|
+
linkText?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Текст примечания. Плейсхолдер `{link}` заменяется ссылкой; если его в
|
|
17
|
+
* тексте нет, ссылка добавляется в конец. Default:
|
|
18
|
+
* {@link DEFAULT_MANAGED_NOTICE_TEXT}.
|
|
19
|
+
*/
|
|
20
|
+
text?: string;
|
|
21
|
+
/** Куда вставлять баннер: 'top' (шапка) или 'bottom' (низ). Default: 'top'. */
|
|
22
|
+
position?: 'top' | 'bottom';
|
|
23
|
+
/**
|
|
24
|
+
* Тип панели Confluence (влияет на цвет/иконку). Default: 'info'.
|
|
25
|
+
* 'warning' — красная, самый заметный вариант для «не редактировать».
|
|
26
|
+
*/
|
|
27
|
+
panel?: 'info' | 'note' | 'warning' | 'tip';
|
|
28
|
+
}
|
|
29
|
+
export declare const DEFAULT_MANAGED_NOTICE_TEXT: string;
|
|
30
|
+
export declare const DEFAULT_MANAGED_NOTICE_LINK_TEXT = "docs-studio";
|
|
31
|
+
/** Строит storage-разметку баннера (одиночный `<ac:structured-macro>`). */
|
|
32
|
+
export declare function buildManagedNotice(opts: ManagedNoticeOptions): string;
|
|
33
|
+
/** Дописывает баннер в начало (top) или конец (bottom) storage-контента. */
|
|
34
|
+
export declare function applyManagedNotice(storage: string, opts: ManagedNoticeOptions): string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* «Баннер управляемой страницы»: примечание о том, что страница ведётся во
|
|
3
|
+
* внешнем источнике (docs-studio), а правки в самой Confluence будут
|
|
4
|
+
* перезаписаны. Вставляется в storage при публикации, но НЕ в markdown-
|
|
5
|
+
* источник — round-trip/экспорт его не увидят, git остаётся чистым.
|
|
6
|
+
*/
|
|
7
|
+
import { structuredMacro } from '../macros/xml.js';
|
|
8
|
+
import { escapeXmlAttr } from '../macros/xml.js';
|
|
9
|
+
export const DEFAULT_MANAGED_NOTICE_TEXT = 'Страница синхронизируется автоматически. Вносите правки в источнике ({link}) — ' +
|
|
10
|
+
'ручные изменения на этой странице будут перезаписаны при следующей публикации.';
|
|
11
|
+
export const DEFAULT_MANAGED_NOTICE_LINK_TEXT = 'docs-studio';
|
|
12
|
+
// Фиксированный ac:macro-id: баннер обязан давать БАЙТ-В-БАЙТ одинаковый
|
|
13
|
+
// storage при каждой публикации. Со случайным id (generateMacroId) content-
|
|
14
|
+
// hash менялся бы каждый раз → страница вечно считалась бы изменённой.
|
|
15
|
+
const NOTICE_MACRO_ID = '0f0e0d0c-0b0a-4009-8008-000000000001';
|
|
16
|
+
/** Собирает `<a href>`-ссылку на источник. */
|
|
17
|
+
function noticeLink(opts) {
|
|
18
|
+
const text = opts.linkText ?? DEFAULT_MANAGED_NOTICE_LINK_TEXT;
|
|
19
|
+
return `<a href="${escapeXmlAttr(opts.linkUrl)}">${escapeXmlAttr(text)}</a>`;
|
|
20
|
+
}
|
|
21
|
+
/** Строит storage-разметку баннера (одиночный `<ac:structured-macro>`). */
|
|
22
|
+
export function buildManagedNotice(opts) {
|
|
23
|
+
if (!opts.linkUrl)
|
|
24
|
+
throw new Error('managedNotice: linkUrl is required');
|
|
25
|
+
const panel = opts.panel ?? 'info';
|
|
26
|
+
const link = noticeLink(opts);
|
|
27
|
+
const text = opts.text ?? DEFAULT_MANAGED_NOTICE_TEXT;
|
|
28
|
+
const inner = text.includes('{link}')
|
|
29
|
+
? text.split('{link}').map(escapeXmlAttr).join(link)
|
|
30
|
+
: `${escapeXmlAttr(text)} ${link}`;
|
|
31
|
+
return structuredMacro(panel, NOTICE_MACRO_ID, { richBody: `<p>${inner}</p>` });
|
|
32
|
+
}
|
|
33
|
+
/** Дописывает баннер в начало (top) или конец (bottom) storage-контента. */
|
|
34
|
+
export function applyManagedNotice(storage, opts) {
|
|
35
|
+
const notice = buildManagedNotice(opts);
|
|
36
|
+
return (opts.position ?? 'top') === 'bottom' ? `${storage}${notice}` : `${notice}${storage}`;
|
|
37
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ConfluenceConfig } from '../client/config.js';
|
|
2
2
|
import { type RenderStorageOptions } from '../markdown/render.js';
|
|
3
|
+
import { type ManagedNoticeOptions } from './notice.js';
|
|
3
4
|
import type { MacroRegistry } from '../macros/registry.js';
|
|
4
5
|
import { Markdown } from '../markdown/markdown.js';
|
|
5
6
|
export interface TableData {
|
|
@@ -42,6 +43,13 @@ export interface PublishPageOptions {
|
|
|
42
43
|
labels?: string[];
|
|
43
44
|
/** Комментарий к версии страницы. */
|
|
44
45
|
versionMessage?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Примечание «страница управляется извне» (docs-studio). Вставляется в
|
|
48
|
+
* storage при публикации, НО не в markdown-источник — экспорт/round-trip
|
|
49
|
+
* его не увидят. Наличие опции включает баннер; настраиваются текст, ссылка,
|
|
50
|
+
* её текст, тип панели и позиция (шапка/низ). См. {@link ManagedNoticeOptions}.
|
|
51
|
+
*/
|
|
52
|
+
managedNotice?: ManagedNoticeOptions;
|
|
45
53
|
/** Свой реестр макросов (default: встроенные core + table-filter). */
|
|
46
54
|
registry?: MacroRegistry;
|
|
47
55
|
/**
|
package/dist/publish/publish.js
CHANGED
|
@@ -8,6 +8,7 @@ import { bpmnOutputName, convertBpmn, isBpmnFile } from '../bpmn/convert.js';
|
|
|
8
8
|
import { downloadToFile, isHttpUrl, remoteFilename } from './remote.js';
|
|
9
9
|
import { renameImagePlaceholders, renderToStorage, } from '../markdown/render.js';
|
|
10
10
|
import { validateMarkdown } from '../markdown/validate.js';
|
|
11
|
+
import { applyManagedNotice } from './notice.js';
|
|
11
12
|
import { processMacros } from '../macros/registry.js';
|
|
12
13
|
import { defaultMacroRegistry } from '../macros/index.js';
|
|
13
14
|
import { Markdown } from '../markdown/markdown.js';
|
|
@@ -85,6 +86,10 @@ export async function publishPage(opts, cfg) {
|
|
|
85
86
|
const tables = opts.tables ?? [];
|
|
86
87
|
const registry = opts.registry ?? defaultMacroRegistry;
|
|
87
88
|
const hashKey = opts.hashPropertyKey ?? DEFAULT_HASH_PROPERTY_KEY;
|
|
89
|
+
// Fail fast: баннер без ссылки бессмыслен — падаем до любого сетевого I/O.
|
|
90
|
+
if (opts.managedNotice && !opts.managedNotice.linkUrl) {
|
|
91
|
+
throw new Error('publishPage: managedNotice.linkUrl is required');
|
|
92
|
+
}
|
|
88
93
|
// 0a. Удалённые источники: http(s)://-элементы в images[]/files[]
|
|
89
94
|
// заменяются на локальный путь в downloadDir с именем из URL — дальше
|
|
90
95
|
// конвейер (валидация, BPMN, аплоад) работает с обычными путями.
|
|
@@ -195,6 +200,13 @@ export async function publishPage(opts, cfg) {
|
|
|
195
200
|
let storage = renderToStorage(markdown, urls, opts.render ?? {});
|
|
196
201
|
// 2.5. Преобразование маркеров макросов в XHTML.
|
|
197
202
|
storage = processMacros(storage, registry).toString();
|
|
203
|
+
// 2.6. Баннер «страница управляется извне». Вставляется здесь, в storage —
|
|
204
|
+
// не в markdown — поэтому источник/round-trip его не содержат. Баннер
|
|
205
|
+
// детерминирован (фиксированный macro-id), значит попадает в content-hash
|
|
206
|
+
// стабильно и не ломает идемпотентность.
|
|
207
|
+
if (opts.managedNotice) {
|
|
208
|
+
storage = applyManagedNotice(storage, opts.managedNotice);
|
|
209
|
+
}
|
|
198
210
|
if (opts.dryRun) {
|
|
199
211
|
console.log(`[publish] dry-run: page ${pageId} rendered OK (${storage.length} bytes of storage)`);
|
|
200
212
|
return {
|
package/dist/publish/runner.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type PublishPageOptions, type PublishPageResult } from './publish.js';
|
|
2
|
+
import type { ManagedNoticeOptions } from './notice.js';
|
|
2
3
|
import { type ConfluenceConfig, type LoadConfigOptions } from '../client/config.js';
|
|
3
4
|
export type Here = (relativePath: string) => string;
|
|
4
5
|
export type Build = (relativePath: string) => string;
|
|
@@ -14,6 +15,12 @@ export interface RunPublishOptions {
|
|
|
14
15
|
* соответствует компоновке репо `<repo>/docs/<set>` + `<repo>/build/<set>`.
|
|
15
16
|
*/
|
|
16
17
|
buildDir?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Баннер «страница управляется извне» по умолчанию для ВСЕХ страниц прогона
|
|
20
|
+
* (docs-studio). Значение, заданное у конкретной страницы в `managedNotice`,
|
|
21
|
+
* имеет приоритет. См. {@link ManagedNoticeOptions}.
|
|
22
|
+
*/
|
|
23
|
+
managedNotice?: ManagedNoticeOptions;
|
|
17
24
|
}
|
|
18
25
|
/**
|
|
19
26
|
* Минимальная точка входа для публикующего скрипта (`docs/<set>/publish.ts`).
|
package/dist/publish/runner.js
CHANGED
|
@@ -29,7 +29,11 @@ export async function runPublish(baseDir, plan, opts = {}) {
|
|
|
29
29
|
const pages = typeof plan === 'function' ? await plan(here, build) : plan;
|
|
30
30
|
const results = [];
|
|
31
31
|
for (const page of pages) {
|
|
32
|
-
|
|
32
|
+
// Дефолтный баннер прогона применяется, только если страница его не задала.
|
|
33
|
+
const merged = opts.managedNotice && page.managedNotice === undefined
|
|
34
|
+
? { ...page, managedNotice: opts.managedNotice }
|
|
35
|
+
: page;
|
|
36
|
+
results.push(await publishPage(merged, cfg));
|
|
33
37
|
}
|
|
34
38
|
return results;
|
|
35
39
|
}
|
package/package.json
CHANGED