confluence-md-sync 0.6.0 → 0.7.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 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
package/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export { Attachment, AttachmentService, toAttachmentVersion, SRC_SHA_SIDECAR_SUF
10
10
  export { Page, Table } from './pages/page.js';
11
11
  export { readTableFromConfluence, findTable, findTableInMacro, parseHtmlTable, decodeHtmlCell, renderMarkdownTable, escapeMdTableCell, readAndMapTable, type ColumnAlign, type TableColumn, } from './pages/tables.js';
12
12
  export { publishPage, computeContentHash, DEFAULT_HASH_PROPERTY_KEY, type PublishPageOptions, type PublishPageResult, type TableData, } from './publish/publish.js';
13
+ export { buildManagedNotice, applyManagedNotice, DEFAULT_MANAGED_NOTICE_TEXT, DEFAULT_MANAGED_NOTICE_LINK_TEXT, type ManagedNoticeOptions, } from './publish/notice.js';
13
14
  export { isHttpUrl, remoteFilename, isSameConfluenceOrigin, remoteRequestHeaders, downloadToFile, } from './publish/remote.js';
14
15
  export { runPublish, type Here, type Build, type PublishPlan, type RunPublishOptions, } from './publish/runner.js';
15
16
  export { parseStorage, serializeStorage, decodeEntities, StorageParseError, type XNode, type XElement, } from './export/xhtml.js';
package/dist/index.js CHANGED
@@ -17,6 +17,7 @@ export { Page, Table } from './pages/page.js';
17
17
  export { readTableFromConfluence, findTable, findTableInMacro, parseHtmlTable, decodeHtmlCell, renderMarkdownTable, escapeMdTableCell, readAndMapTable, } from './pages/tables.js';
18
18
  // Publish
19
19
  export { publishPage, computeContentHash, DEFAULT_HASH_PROPERTY_KEY, } from './publish/publish.js';
20
+ export { buildManagedNotice, applyManagedNotice, DEFAULT_MANAGED_NOTICE_TEXT, DEFAULT_MANAGED_NOTICE_LINK_TEXT, } from './publish/notice.js';
20
21
  export { isHttpUrl, remoteFilename, isSameConfluenceOrigin, remoteRequestHeaders, downloadToFile, } from './publish/remote.js';
21
22
  export { runPublish, } from './publish/runner.js';
22
23
  // Export (storage → markdown) & round-trip
@@ -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
  /**
@@ -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 {
@@ -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`).
@@ -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
- results.push(await publishPage(page, cfg));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "confluence-md-sync",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Publish Markdown to Confluence (Data Center & Cloud): idempotent page sync, attachment dedup, tables and a pluggable macro system",
5
5
  "keywords": [
6
6
  "confluence",