confluence-md-sync 0.5.0 → 0.5.1

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.
@@ -557,9 +557,12 @@ class Converter {
557
557
  throw new Unrepresentable();
558
558
  const grid = [];
559
559
  const aligns = [];
560
+ const headerFlags = [];
560
561
  trs.forEach((tr, rowIdx) => {
561
562
  if (!grid[rowIdx])
562
563
  grid[rowIdx] = [];
564
+ const rowCells = elements(tr.children).filter((c) => c.name === 'td' || c.name === 'th');
565
+ headerFlags[rowIdx] = rowCells.length > 0 && rowCells.every((c) => c.name === 'th');
563
566
  let col = 0;
564
567
  for (const cell of elements(tr.children)) {
565
568
  if (cell.name !== 'td' && cell.name !== 'th')
@@ -596,7 +599,9 @@ class Converter {
596
599
  }
597
600
  while (aligns.length < width)
598
601
  aligns.push('none');
599
- return { grid, aligns, caption };
602
+ while (headerFlags.length < grid.length)
603
+ headerFlags.push(false);
604
+ return { grid, aligns, caption, headerFlags };
600
605
  }
601
606
  /** Разворачивает любую таблицу в GFM (сетка с заполнением объединений). */
602
607
  readableTable(table) {
@@ -611,29 +616,61 @@ class Converter {
611
616
  /**
612
617
  * tables:'records' — каждую строку тела таблицы разворачивает в запись
613
618
  * «**Заголовок:** значение» (пустые ячейки и колонки-филлеры пропускаются),
614
- * записи разделяются `---`. Заголовки берутся из первой строки сетки.
619
+ * записи разделяются `---`.
620
+ *
621
+ * Заголовок — ПОСЛЕДНЯЯ из ведущих строк, целиком состоящих из <th>
622
+ * (строки-титулы над ней, например «Детали» с colspan, уходят в подпись).
623
+ * Многострочное значение ячейки разворачивается под заголовком: <br> →
624
+ * перенос строки, буллеты «• » → элементы md-списка.
615
625
  */
616
626
  recordsTable(table) {
617
627
  this.stats.lossy++;
618
- const { grid, caption } = this.buildTableGrid(table);
619
- const header = grid[0];
628
+ const { grid, caption, headerFlags } = this.buildTableGrid(table);
629
+ // Ведущие строки-заголовки: последняя из них — ключи, предыдущие — титулы.
630
+ let headerCount = 0;
631
+ while (headerCount < grid.length && headerFlags[headerCount])
632
+ headerCount++;
633
+ if (headerCount === 0)
634
+ headerCount = 1; // нет <th>-шапки → первая строка как ключи
635
+ if (headerCount >= grid.length)
636
+ headerCount = 1; // не съедать всю таблицу
637
+ const header = grid[headerCount - 1];
638
+ const titleCells = grid
639
+ .slice(0, headerCount - 1)
640
+ .flat()
641
+ .map((c) => c.trim())
642
+ .filter((c) => c !== '');
643
+ const title = [caption, ...titleCells].filter((c) => c !== '').join(' — ');
620
644
  const records = [];
621
- for (const row of grid.slice(1)) {
622
- const lines = [];
645
+ for (const row of grid.slice(headerCount)) {
646
+ const fields = [];
623
647
  row.forEach((val, i) => {
624
648
  const key = (header[i] ?? '').trim();
625
- // В записи <br> из ячейки → «; » (одна строка, без HTML).
626
- const value = val.replace(/<br>/g, '; ').trim();
627
- if (key !== '' && value !== '')
628
- lines.push(`**${key}:** ${value}`);
649
+ if (key === '')
650
+ return;
651
+ // Значение записи свободный Markdown (не GFM-ячейка): <br> →
652
+ // настоящий перенос, «• » → элементы списка; многострочное значение
653
+ // выносим под заголовок.
654
+ const value = val
655
+ .replace(/<br>/g, '\n')
656
+ .replace(/(^|\n)• /g, '$1- ')
657
+ .trim();
658
+ if (value === '')
659
+ return;
660
+ fields.push(value.includes('\n') ? `**${key}:**\n${value}` : `**${key}:** ${value}`);
629
661
  });
630
- if (lines.length > 0)
631
- records.push(lines.join('\n'));
662
+ // Одиночные значения — поля подряд (одна строка на поле); если есть
663
+ // многострочные (списки), разделяем пустой строкой, чтобы md-список не
664
+ // «склеивался» со следующим заголовком.
665
+ if (fields.length > 0) {
666
+ const sep = fields.some((f) => f.includes('\n')) ? '\n\n' : '\n';
667
+ records.push(fields.join(sep));
668
+ }
632
669
  }
633
670
  if (records.length === 0)
634
671
  throw new Unrepresentable();
635
672
  const body = records.join('\n\n---\n\n');
636
- return caption !== '' ? `**${caption}**\n\n${body}` : body;
673
+ return title !== '' ? `**${title}**\n\n${body}` : body;
637
674
  }
638
675
  /**
639
676
  * Сплющивает содержимое ячейки в одну строку: абзацы и пункты списков
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "confluence-md-sync",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
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",