confluence-md-sync 0.5.0 → 0.5.2

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,62 @@ 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
+ // Нет <th>-шапки (или вся таблица — заголовки): в записи разворачивать
634
+ // нечего — первая строка данных стала бы «ключами» (в т.ч. картинка →
635
+ // ключ `**<img …>:**`). Такую таблицу отдаём как GFM.
636
+ if (headerCount === 0 || headerCount >= grid.length)
637
+ return this.readableTable(table);
638
+ const header = grid[headerCount - 1];
639
+ const titleCells = grid
640
+ .slice(0, headerCount - 1)
641
+ .flat()
642
+ .map((c) => c.trim())
643
+ .filter((c) => c !== '');
644
+ const title = [caption, ...titleCells].filter((c) => c !== '').join(' — ');
620
645
  const records = [];
621
- for (const row of grid.slice(1)) {
622
- const lines = [];
646
+ for (const row of grid.slice(headerCount)) {
647
+ const fields = [];
623
648
  row.forEach((val, i) => {
624
649
  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}`);
650
+ if (key === '')
651
+ return;
652
+ // Значение записи свободный Markdown (не GFM-ячейка): <br> →
653
+ // настоящий перенос, «• » → элементы списка; многострочное значение
654
+ // выносим под заголовок.
655
+ const value = val
656
+ .replace(/<br>/g, '\n')
657
+ .replace(/(^|\n)• /g, '$1- ')
658
+ .trim();
659
+ if (value === '')
660
+ return;
661
+ fields.push(value.includes('\n') ? `**${key}:**\n${value}` : `**${key}:** ${value}`);
629
662
  });
630
- if (lines.length > 0)
631
- records.push(lines.join('\n'));
663
+ // Одиночные значения — поля подряд (одна строка на поле); если есть
664
+ // многострочные (списки), разделяем пустой строкой, чтобы md-список не
665
+ // «склеивался» со следующим заголовком.
666
+ if (fields.length > 0) {
667
+ const sep = fields.some((f) => f.includes('\n')) ? '\n\n' : '\n';
668
+ records.push(fields.join(sep));
669
+ }
632
670
  }
633
671
  if (records.length === 0)
634
672
  throw new Unrepresentable();
635
673
  const body = records.join('\n\n---\n\n');
636
- return caption !== '' ? `**${caption}**\n\n${body}` : body;
674
+ return title !== '' ? `**${title}**\n\n${body}` : body;
637
675
  }
638
676
  /**
639
677
  * Сплющивает содержимое ячейки в одну строку: абзацы и пункты списков
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.2",
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",