confluence-md-sync 0.8.1 → 0.8.3
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.
|
@@ -25,6 +25,7 @@ import { processMacros } from '../macros/registry.js';
|
|
|
25
25
|
import { defaultMacroRegistry } from '../macros/index.js';
|
|
26
26
|
import { renderToStorage } from '../markdown/render.js';
|
|
27
27
|
import { compareStorage } from './canonical.js';
|
|
28
|
+
import { NOTICE_MACRO_ID } from '../publish/notice.js';
|
|
28
29
|
import { decodeEntities, elements, getAttr, hasNamespacedElements, parseStorage, serializeStorage, textContent, } from './xhtml.js';
|
|
29
30
|
/** Конвертирует storage-фрагмент страницы в Markdown. */
|
|
30
31
|
export function storageToMarkdown(storage, opts = {}) {
|
|
@@ -283,7 +284,14 @@ class Converter {
|
|
|
283
284
|
}
|
|
284
285
|
// p / td / th / li / caption и прочие «инлайн-контейнеры» → инлайн.
|
|
285
286
|
// multiline: это свободный поток (не ячейка) — <br> станет переносом.
|
|
286
|
-
|
|
287
|
+
let inline = '';
|
|
288
|
+
try {
|
|
289
|
+
inline = this.inlineToMd(el.children, { multiline: true }).trim();
|
|
290
|
+
}
|
|
291
|
+
catch (e) {
|
|
292
|
+
if (!(e instanceof Unrepresentable))
|
|
293
|
+
throw e; /* → голый текст ниже */
|
|
294
|
+
}
|
|
287
295
|
if (inline !== '')
|
|
288
296
|
return inline.split('\n').map((l) => guardLineStart(l)).join('\n');
|
|
289
297
|
// Совсем ничего не вышло — голый текст (может быть пустым).
|
|
@@ -301,8 +309,6 @@ class Converter {
|
|
|
301
309
|
anchor: 'anchor', detailssummary: 'properties-report',
|
|
302
310
|
};
|
|
303
311
|
tryNativeMd(el, name) {
|
|
304
|
-
if (this.readable)
|
|
305
|
-
return null; // readable-путь остаётся прежним
|
|
306
312
|
const parts = this.macroParts(el);
|
|
307
313
|
if (parts === null)
|
|
308
314
|
return null;
|
|
@@ -355,7 +361,7 @@ class Converter {
|
|
|
355
361
|
if (norm === null)
|
|
356
362
|
return null;
|
|
357
363
|
const cand = `::: properties${dp.join('')}\n${norm.md}\n:::`;
|
|
358
|
-
if (!this.verifyNormalizedDetails(cand, norm.rows))
|
|
364
|
+
if (!this.readable && !this.verifyNormalizedDetails(cand, norm.rows))
|
|
359
365
|
return null;
|
|
360
366
|
this.stats.normalized++;
|
|
361
367
|
return cand; // канонической эквивалентности нет по построению — верифицирован текст
|
|
@@ -402,6 +408,8 @@ class Converter {
|
|
|
402
408
|
}
|
|
403
409
|
if (candidate === null)
|
|
404
410
|
return null;
|
|
411
|
+
if (this.readable)
|
|
412
|
+
return candidate; // readable: без канонической верификации (режим и так lossy)
|
|
405
413
|
return this.verifyMacroMarker(el, candidate) ? candidate : null;
|
|
406
414
|
}
|
|
407
415
|
/** Разбирает macro-элемент на параметры и тела; null — посторонние дети. */
|
|
@@ -438,6 +446,9 @@ class Converter {
|
|
|
438
446
|
}
|
|
439
447
|
}
|
|
440
448
|
macroToMd(el) {
|
|
449
|
+
// Баннер managedNotice — артефакт публикации, в md-источник не попадает никогда.
|
|
450
|
+
if (getAttr(el, 'ac:macro-id') === NOTICE_MACRO_ID)
|
|
451
|
+
return '';
|
|
441
452
|
const name = getAttr(el, 'ac:name') ?? '';
|
|
442
453
|
const nativeMd = this.tryNativeMd(el, name);
|
|
443
454
|
if (nativeMd !== null) {
|
|
@@ -1226,6 +1237,21 @@ class Converter {
|
|
|
1226
1237
|
return `{{img:${filename}${attrStr}}}`;
|
|
1227
1238
|
}
|
|
1228
1239
|
acLinkToMd(el) {
|
|
1240
|
+
// readable безотказен: непредставимая ссылка (user-mention, ссылка без цели,
|
|
1241
|
+
// card-appearance) деградирует до своего текста.
|
|
1242
|
+
if (this.readable) {
|
|
1243
|
+
try {
|
|
1244
|
+
return this.acLinkStrict(el);
|
|
1245
|
+
}
|
|
1246
|
+
catch (e) {
|
|
1247
|
+
if (!(e instanceof Unrepresentable))
|
|
1248
|
+
throw e;
|
|
1249
|
+
return escapeMdText(textContent(el.children), {}, true).trim();
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
return this.acLinkStrict(el);
|
|
1253
|
+
}
|
|
1254
|
+
acLinkStrict(el) {
|
|
1229
1255
|
if (el.attrs.length > 0)
|
|
1230
1256
|
throw new Unrepresentable();
|
|
1231
1257
|
const kids = elements(el.children);
|
|
@@ -1446,8 +1472,32 @@ function escapeMdText(raw, ctx, readable = false) {
|
|
|
1446
1472
|
out += escapePlain(collapsed.slice(last), ctx, readable);
|
|
1447
1473
|
return out;
|
|
1448
1474
|
}
|
|
1475
|
+
const ESCAPE_SPECIALS = '\\`*[]{}~';
|
|
1476
|
+
// Буква/цифра любого алфавита (вкл. кириллицу). Внутрисловное `_` между двумя
|
|
1477
|
+
// такими символами по CommonMark не открывает и не закрывает акцент, поэтому
|
|
1478
|
+
// экранировать его НЕ нужно.
|
|
1479
|
+
const WORDCHAR_RE = /[\p{L}\p{N}]/u;
|
|
1449
1480
|
function escapePlain(s, _ctx, readable = false) {
|
|
1450
|
-
|
|
1481
|
+
let esc = '';
|
|
1482
|
+
for (let i = 0; i < s.length; i++) {
|
|
1483
|
+
const c = s[i];
|
|
1484
|
+
if (c === '_') {
|
|
1485
|
+
// Экранируем `_` только на границе слова (snake_case НЕ трогаем — иначе
|
|
1486
|
+
// выходят уродливые `BA\_M1\_…`). Внутрисловное `_` литерально и в
|
|
1487
|
+
// CommonMark, round-trip остаётся канонически эквивалентным.
|
|
1488
|
+
const prev = s[i - 1];
|
|
1489
|
+
const next = s[i + 1];
|
|
1490
|
+
const intraword = prev !== undefined && next !== undefined &&
|
|
1491
|
+
WORDCHAR_RE.test(prev) && WORDCHAR_RE.test(next);
|
|
1492
|
+
esc += intraword ? '_' : '\\_';
|
|
1493
|
+
}
|
|
1494
|
+
else if (ESCAPE_SPECIALS.includes(c)) {
|
|
1495
|
+
esc += '\\' + c;
|
|
1496
|
+
}
|
|
1497
|
+
else {
|
|
1498
|
+
esc += c;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1451
1501
|
// Пайпы здесь НЕ трогаем — они экранируются один раз на границе ячейки
|
|
1452
1502
|
// (cellMd / readableTable), иначе плейсхолдеры {{img:…|…}} двоились бы.
|
|
1453
1503
|
// readable: неразрывный пробел → обычный; faithful: → entity (переживает
|
package/dist/publish/notice.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export interface ManagedNoticeOptions {
|
|
|
28
28
|
}
|
|
29
29
|
export declare const DEFAULT_MANAGED_NOTICE_TEXT: string;
|
|
30
30
|
export declare const DEFAULT_MANAGED_NOTICE_LINK_TEXT = "docs-studio";
|
|
31
|
+
export declare const NOTICE_MACRO_ID = "0f0e0d0c-0b0a-4009-8008-000000000001";
|
|
31
32
|
/** Строит storage-разметку баннера (одиночный `<ac:structured-macro>`). */
|
|
32
33
|
export declare function buildManagedNotice(opts: ManagedNoticeOptions): string;
|
|
33
34
|
/** Дописывает баннер в начало (top) или конец (bottom) storage-контента. */
|
package/dist/publish/notice.js
CHANGED
|
@@ -12,7 +12,7 @@ export const DEFAULT_MANAGED_NOTICE_LINK_TEXT = 'docs-studio';
|
|
|
12
12
|
// Фиксированный ac:macro-id: баннер обязан давать БАЙТ-В-БАЙТ одинаковый
|
|
13
13
|
// storage при каждой публикации. Со случайным id (generateMacroId) content-
|
|
14
14
|
// hash менялся бы каждый раз → страница вечно считалась бы изменённой.
|
|
15
|
-
const NOTICE_MACRO_ID = '0f0e0d0c-0b0a-4009-8008-000000000001';
|
|
15
|
+
export const NOTICE_MACRO_ID = '0f0e0d0c-0b0a-4009-8008-000000000001';
|
|
16
16
|
/** Собирает `<a href>`-ссылку на источник. */
|
|
17
17
|
function noticeLink(opts) {
|
|
18
18
|
const text = opts.linkText ?? DEFAULT_MANAGED_NOTICE_LINK_TEXT;
|
package/package.json
CHANGED