confluence-md-sync 0.1.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.
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/dist/attachments/attachment.d.ts +39 -0
- package/dist/attachments/attachment.js +83 -0
- package/dist/attachments/hash.d.ts +2 -0
- package/dist/attachments/hash.js +6 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +85 -0
- package/dist/client/client.d.ts +128 -0
- package/dist/client/client.js +311 -0
- package/dist/client/config.d.ts +42 -0
- package/dist/client/config.js +54 -0
- package/dist/csv.d.ts +18 -0
- package/dist/csv.js +86 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +20 -0
- package/dist/macros/builder.d.ts +30 -0
- package/dist/macros/builder.js +58 -0
- package/dist/macros/index.d.ts +44 -0
- package/dist/macros/index.js +50 -0
- package/dist/macros/plugins/core.d.ts +49 -0
- package/dist/macros/plugins/core.js +214 -0
- package/dist/macros/plugins/table-filter.d.ts +35 -0
- package/dist/macros/plugins/table-filter.js +155 -0
- package/dist/macros/registry.d.ts +29 -0
- package/dist/macros/registry.js +111 -0
- package/dist/macros/types.d.ts +38 -0
- package/dist/macros/types.js +15 -0
- package/dist/macros/xml.d.ts +28 -0
- package/dist/macros/xml.js +52 -0
- package/dist/markdown/markdown.d.ts +36 -0
- package/dist/markdown/markdown.js +78 -0
- package/dist/markdown/render.d.ts +35 -0
- package/dist/markdown/render.js +80 -0
- package/dist/markdown/validate.d.ts +11 -0
- package/dist/markdown/validate.js +60 -0
- package/dist/pages/page.d.ts +104 -0
- package/dist/pages/page.js +234 -0
- package/dist/pages/tables.d.ts +72 -0
- package/dist/pages/tables.js +163 -0
- package/dist/publish/publish.d.ts +67 -0
- package/dist/publish/publish.js +164 -0
- package/dist/publish/runner.d.ts +36 -0
- package/dist/publish/runner.js +41 -0
- package/dist/wrapper.d.ts +21 -0
- package/dist/wrapper.js +36 -0
- package/package.json +69 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Объектная модель для работы со страницами и таблицами Confluence.
|
|
3
|
+
*/
|
|
4
|
+
import { findTable, findTableInMacro, renderMarkdownTable } from './tables.js';
|
|
5
|
+
import { Attachment, toAttachmentVersion } from '../attachments/attachment.js';
|
|
6
|
+
/**
|
|
7
|
+
* Представляет страницу Confluence.
|
|
8
|
+
*/
|
|
9
|
+
export class Page {
|
|
10
|
+
id;
|
|
11
|
+
title;
|
|
12
|
+
storage;
|
|
13
|
+
client;
|
|
14
|
+
constructor(id, title, storage, client) {
|
|
15
|
+
this.id = id;
|
|
16
|
+
this.title = title;
|
|
17
|
+
this.storage = storage;
|
|
18
|
+
this.client = client;
|
|
19
|
+
}
|
|
20
|
+
/** Сырой storage-контент страницы. */
|
|
21
|
+
getStorage() {
|
|
22
|
+
return this.storage;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Скачивает аттач страницы по имени файла. Возвращает сырые байты.
|
|
26
|
+
* Текстовая декодировка — через {@link getAttachmentText}.
|
|
27
|
+
*/
|
|
28
|
+
async getAttachment(filename) {
|
|
29
|
+
const list = await this.client.listAttachments(this.id, filename);
|
|
30
|
+
const att = list.find((a) => a.title === filename);
|
|
31
|
+
if (!att)
|
|
32
|
+
throw new Error(`Attachment '${filename}' not found on page ${this.id}`);
|
|
33
|
+
const path = att._links?.download;
|
|
34
|
+
if (!path)
|
|
35
|
+
throw new Error(`Attachment '${filename}' on page ${this.id} has no download link`);
|
|
36
|
+
return this.client.downloadAttachment(path);
|
|
37
|
+
}
|
|
38
|
+
/** Скачивает аттач и декодирует как текст (по умолчанию utf-8). */
|
|
39
|
+
async getAttachmentText(filename, encoding = 'utf-8') {
|
|
40
|
+
const buf = await this.getAttachment(filename);
|
|
41
|
+
return buf.toString(encoding);
|
|
42
|
+
}
|
|
43
|
+
/** Список аттачей страницы с историей версий (versions заполнены). */
|
|
44
|
+
async getAttachments() {
|
|
45
|
+
const list = await this.client.listAttachments(this.id);
|
|
46
|
+
return Promise.all(list.map(async (a) => {
|
|
47
|
+
const raw = await this.client.getAttachmentVersions(a.id);
|
|
48
|
+
const versions = raw
|
|
49
|
+
.map(toAttachmentVersion)
|
|
50
|
+
.sort((x, y) => y.version - x.version);
|
|
51
|
+
const downloadUrl = a._links?.download
|
|
52
|
+
? this.client.absoluteUrl(a._links.download)
|
|
53
|
+
: this.client.absoluteUrl(`/download/attachments/${this.id}/${encodeURIComponent(a.title)}`);
|
|
54
|
+
return new Attachment(a.id, this.id, a.title, a.version?.number ?? 0, downloadUrl, versions);
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Удаляет у всех аттачей страницы старые версии, оставляя только последнюю
|
|
59
|
+
* (max по номеру). Полезно для регулярно перезаписываемых файлов (CSV),
|
|
60
|
+
* которые иначе копят десятки версий. Возвращает число удалённых версий.
|
|
61
|
+
*/
|
|
62
|
+
async removeOldAttachmentVersions() {
|
|
63
|
+
const attachments = await this.getAttachments();
|
|
64
|
+
let removed = 0;
|
|
65
|
+
for (const att of attachments) {
|
|
66
|
+
if (att.versions.length <= 1)
|
|
67
|
+
continue;
|
|
68
|
+
const latest = Math.max(...att.versions.map((v) => v.version));
|
|
69
|
+
for (const v of att.versions) {
|
|
70
|
+
if (v.version >= latest)
|
|
71
|
+
continue;
|
|
72
|
+
try {
|
|
73
|
+
await this.client.removeAttachmentVersion(this.id, att.title, v.version);
|
|
74
|
+
removed++;
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
console.warn(`[attachments] не удалось удалить ${att.title} v${v.version}: ${err.message}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return removed;
|
|
82
|
+
}
|
|
83
|
+
/** Лейблы страницы. */
|
|
84
|
+
async getLabels() {
|
|
85
|
+
return this.client.getLabels(this.id);
|
|
86
|
+
}
|
|
87
|
+
/** Добавляет лейблы (идемпотентно). */
|
|
88
|
+
async addLabels(labels) {
|
|
89
|
+
await this.client.addLabels(this.id, labels);
|
|
90
|
+
}
|
|
91
|
+
/** Дочерние страницы. */
|
|
92
|
+
async getChildren() {
|
|
93
|
+
return this.client.getChildPages(this.id);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Извлекает таблицу со страницы.
|
|
97
|
+
*
|
|
98
|
+
* @param target порядковый номер таблицы (0 = первая) или имя
|
|
99
|
+
* table-excerpt-макроса, обёрнутого вокруг таблицы (параметр `name`).
|
|
100
|
+
* @throws если таблица не найдена
|
|
101
|
+
*/
|
|
102
|
+
getTable(target = 0) {
|
|
103
|
+
const tableHtml = typeof target === 'string'
|
|
104
|
+
? findTableInMacro(this.storage, target)
|
|
105
|
+
: findTable(this.storage, target);
|
|
106
|
+
if (!tableHtml) {
|
|
107
|
+
const desc = typeof target === 'string' ? `macro name="${target}"` : `index ${target}`;
|
|
108
|
+
throw new Error(`Table not found on page ${this.id} (${desc})`);
|
|
109
|
+
}
|
|
110
|
+
return new Table(tableHtml);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Извлекает все таблицы со страницы.
|
|
114
|
+
*/
|
|
115
|
+
getTables() {
|
|
116
|
+
const tables = [];
|
|
117
|
+
let index = 0;
|
|
118
|
+
while (true) {
|
|
119
|
+
try {
|
|
120
|
+
tables.push(this.getTable(index));
|
|
121
|
+
index++;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return tables;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Представляет таблицу Confluence (извлечённую из HTML storage format).
|
|
132
|
+
*
|
|
133
|
+
* Предоставляет методы для преобразования таблицы в разные форматы:
|
|
134
|
+
* - `toCells()` — массив строк и ячеек
|
|
135
|
+
* - `toAny()` — массив объектов с заголовками как ключи
|
|
136
|
+
* - `toType<T>()` — массив типизированных объектов с маппером
|
|
137
|
+
*/
|
|
138
|
+
export class Table {
|
|
139
|
+
cells;
|
|
140
|
+
headers;
|
|
141
|
+
constructor(tableHtml) {
|
|
142
|
+
this.cells = tableHtml;
|
|
143
|
+
this.headers = this.cells[0] || [];
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Возвращает сырой массив ячеек таблицы.
|
|
147
|
+
*/
|
|
148
|
+
toCells() {
|
|
149
|
+
return this.cells;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Возвращает строки таблицы (без заголовков) как массив объектов.
|
|
153
|
+
*
|
|
154
|
+
* Ключи объектов — заголовки колонок.
|
|
155
|
+
* Если есть дублирующиеся заголовки, к имени добавляется порядковый номер.
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* table.toAny() → [
|
|
159
|
+
* { 'Таб.№': '14800145', 'ФИО': 'Иванов И.И.', ... },
|
|
160
|
+
* { 'Таб.№': '14800146', 'ФИО': 'Петров П.П.', ... },
|
|
161
|
+
* ]
|
|
162
|
+
*/
|
|
163
|
+
toAny() {
|
|
164
|
+
const headerMap = this.buildHeaderMap();
|
|
165
|
+
return this.cells.slice(1).map((row) => {
|
|
166
|
+
const obj = {};
|
|
167
|
+
row.forEach((cell, i) => {
|
|
168
|
+
const key = headerMap.get(i) || `[${i}]`;
|
|
169
|
+
obj[key] = cell;
|
|
170
|
+
});
|
|
171
|
+
return obj;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Преобразует таблицу в массив типизированных объектов.
|
|
176
|
+
*
|
|
177
|
+
* @param mapper функция, которая заполняет поля результирующего объекта на основе строки таблицы
|
|
178
|
+
* @returns массив объектов типа T, заполненный маппером
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* const employees = table.toType<Employee>((row, emp) => {
|
|
182
|
+
* emp.tabNumber = row['Таб.№'];
|
|
183
|
+
* emp.fio = row['ФИО'];
|
|
184
|
+
* });
|
|
185
|
+
*/
|
|
186
|
+
toType(mapper) {
|
|
187
|
+
const rows = this.toAny();
|
|
188
|
+
const results = [];
|
|
189
|
+
for (const row of rows) {
|
|
190
|
+
const result = {};
|
|
191
|
+
mapper(row, result);
|
|
192
|
+
results.push(result);
|
|
193
|
+
}
|
|
194
|
+
return results;
|
|
195
|
+
}
|
|
196
|
+
/** Возвращает количество строк (включая заголовок). */
|
|
197
|
+
get rowCount() {
|
|
198
|
+
return this.cells.length;
|
|
199
|
+
}
|
|
200
|
+
/** Возвращает количество колонок. */
|
|
201
|
+
get columnCount() {
|
|
202
|
+
return this.headers.length;
|
|
203
|
+
}
|
|
204
|
+
/** Возвращает заголовки колонок. */
|
|
205
|
+
getHeaders() {
|
|
206
|
+
return [...this.headers];
|
|
207
|
+
}
|
|
208
|
+
/** Возвращает таблицу в Markdown формате для использования с макросами. */
|
|
209
|
+
toMarkdown() {
|
|
210
|
+
const items = this.toAny();
|
|
211
|
+
const columns = this.headers.map((header) => ({
|
|
212
|
+
header,
|
|
213
|
+
cell: (item) => item[header] || '',
|
|
214
|
+
}));
|
|
215
|
+
return renderMarkdownTable(items, columns);
|
|
216
|
+
}
|
|
217
|
+
/** Строит карту заголовков с обработкой дубликатов. */
|
|
218
|
+
buildHeaderMap() {
|
|
219
|
+
const map = new Map();
|
|
220
|
+
const seen = new Map();
|
|
221
|
+
this.headers.forEach((header, i) => {
|
|
222
|
+
const normalized = header.toLowerCase();
|
|
223
|
+
const count = (seen.get(normalized) || 0) + 1;
|
|
224
|
+
seen.set(normalized, count);
|
|
225
|
+
if (count === 1) {
|
|
226
|
+
map.set(i, header);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
map.set(i, `${header} (${count})`);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
return map;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ConfluenceConfig } from '../client/config.js';
|
|
2
|
+
import { Markdown } from '../markdown/markdown.js';
|
|
3
|
+
/** Читает таблицу со страницы Confluence (storage format) и возвращает `string[][]`. */
|
|
4
|
+
export declare function readTableFromConfluence(pageId: string, cfg: ConfluenceConfig, tableIndex?: number): Promise<string[][]>;
|
|
5
|
+
/**
|
|
6
|
+
* Ищет таблицу в Confluence storage XHTML по индексу (первая таблица = 0).
|
|
7
|
+
* Возвращает `string[][]` с ячейками таблицы или null, если не найдена.
|
|
8
|
+
*/
|
|
9
|
+
export declare function findTable(storage: string, index?: number): string[][] | null;
|
|
10
|
+
/**
|
|
11
|
+
* Ищет первую таблицу внутри table-excerpt-макроса с заданным значением
|
|
12
|
+
* параметра `name`. Возвращает ячейки или null, если такого макроса
|
|
13
|
+
* (или таблицы внутри него) нет.
|
|
14
|
+
*/
|
|
15
|
+
export declare function findTableInMacro(storage: string, name: string): string[][] | null;
|
|
16
|
+
/**
|
|
17
|
+
* Парсит Confluence storage XHTML, извлекает её таблицу и возвращает
|
|
18
|
+
* её ячейки как `string[][]` (где каждый элемент уже декодирован из HTML).
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseHtmlTable(storage: string): string[][];
|
|
21
|
+
/**
|
|
22
|
+
* Декодирует ячейку HTML-таблицы: удаляет теги, декодирует сущности,
|
|
23
|
+
* сжимает пробелы, триммирует. Подходит для использования в парсинге
|
|
24
|
+
* storage format из Confluence.
|
|
25
|
+
*
|
|
26
|
+
* Порядок декодирования важен: `&` обрабатывается ПОСЛЕДНИМ, чтобы
|
|
27
|
+
* не разрушить уже декодированные сущности типа `<`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function decodeHtmlCell(html: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Читает таблицу из Confluence и преобразует её в типизированный массив.
|
|
32
|
+
* Первая строка таблицы считается заголовками.
|
|
33
|
+
*
|
|
34
|
+
* @param pageId ID страницы Confluence
|
|
35
|
+
* @param cfg конфиг Confluence
|
|
36
|
+
* @param mapRow функция, которая преобразует (строка: string[], заголовки: string[]) → T
|
|
37
|
+
* @param tableIndex индекс таблицы на странице (0 = первая)
|
|
38
|
+
* @returns типизированный массив строк таблицы (без заголовков)
|
|
39
|
+
* @throws если таблица пустая или содержит только заголовки
|
|
40
|
+
*/
|
|
41
|
+
export declare function readAndMapTable<T>(pageId: string, cfg: ConfluenceConfig, mapRow: (row: string[], headers: string[]) => T, tableIndex?: number): Promise<T[]>;
|
|
42
|
+
export type ColumnAlign = 'left' | 'right' | 'center';
|
|
43
|
+
export interface TableColumn<T> {
|
|
44
|
+
header: string;
|
|
45
|
+
cell: (item: T) => string;
|
|
46
|
+
/** Выравнивание колонки в опубликованной таблице. По умолчанию — left. */
|
|
47
|
+
align?: ColumnAlign;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Рендерит типизированный массив в Markdown-таблицу на основе описания
|
|
51
|
+
* колонок. Каждая колонка задаёт заголовок, функцию извлечения значения
|
|
52
|
+
* и опциональное выравнивание. Выравнивание транслируется markdown-it'ом
|
|
53
|
+
* в `style="text-align: right|center|left"` на <th>/<td>, что Confluence
|
|
54
|
+
* сохраняет в storage.
|
|
55
|
+
*
|
|
56
|
+
* @param items массив элементов; если пусто, возвращается сообщение «нет данных»
|
|
57
|
+
* @param columns описание колонок; не может быть пусто
|
|
58
|
+
* @throws если columns пусто
|
|
59
|
+
* @example
|
|
60
|
+
* interface Row { name: string; hours: number; }
|
|
61
|
+
* const cols: TableColumn<Row>[] = [
|
|
62
|
+
* { header: 'Имя', cell: (r) => r.name },
|
|
63
|
+
* { header: 'Часы', cell: (r) => r.hours.toFixed(2), align: 'right' },
|
|
64
|
+
* ];
|
|
65
|
+
* const md = renderMarkdownTable(rows, cols);
|
|
66
|
+
*/
|
|
67
|
+
export declare function renderMarkdownTable<T>(items: T[], columns: Array<TableColumn<T>>): Markdown;
|
|
68
|
+
/**
|
|
69
|
+
* Экранирует спецсимволы для использования в ячейке Markdown-таблицы:
|
|
70
|
+
* заменяет `|` на `\|`, переносы на пробелы.
|
|
71
|
+
*/
|
|
72
|
+
export declare function escapeMdTableCell(s: string): string;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { ConfluenceClient } from '../client/client.js';
|
|
2
|
+
import { Markdown } from '../markdown/markdown.js';
|
|
3
|
+
/** Читает таблицу со страницы Confluence (storage format) и возвращает `string[][]`. */
|
|
4
|
+
export async function readTableFromConfluence(pageId, cfg, tableIndex = 0) {
|
|
5
|
+
const client = new ConfluenceClient(cfg);
|
|
6
|
+
const { storage } = await client.getPageStorage(pageId);
|
|
7
|
+
const table = findTable(storage, tableIndex);
|
|
8
|
+
if (!table) {
|
|
9
|
+
const tableCount = (storage.match(/<table/g) || []).length;
|
|
10
|
+
throw new Error(tableCount === 0
|
|
11
|
+
? `readTableFromConfluence(${pageId}): no tables found on page`
|
|
12
|
+
: `readTableFromConfluence(${pageId}): table ${tableIndex} not found (page has ${tableCount} table(s))`);
|
|
13
|
+
}
|
|
14
|
+
return table;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Ищет таблицу в Confluence storage XHTML по индексу (первая таблица = 0).
|
|
18
|
+
* Возвращает `string[][]` с ячейками таблицы или null, если не найдена.
|
|
19
|
+
*/
|
|
20
|
+
export function findTable(storage, index = 0) {
|
|
21
|
+
let tableIdx = 0;
|
|
22
|
+
for (const tableMatch of storage.matchAll(/<table[^>]*>([\s\S]*?)<\/table>/g)) {
|
|
23
|
+
if (tableIdx === index) {
|
|
24
|
+
return parseHtmlTable(tableMatch[1]);
|
|
25
|
+
}
|
|
26
|
+
tableIdx++;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Ищет первую таблицу внутри table-excerpt-макроса с заданным значением
|
|
32
|
+
* параметра `name`. Возвращает ячейки или null, если такого макроса
|
|
33
|
+
* (или таблицы внутри него) нет.
|
|
34
|
+
*/
|
|
35
|
+
export function findTableInMacro(storage, name) {
|
|
36
|
+
const macroRe = /<ac:structured-macro[^>]*ac:name="table-excerpt"[^>]*>([\s\S]*?)<\/ac:structured-macro>/g;
|
|
37
|
+
for (const macroMatch of storage.matchAll(macroRe)) {
|
|
38
|
+
const inner = macroMatch[1];
|
|
39
|
+
const nameRe = /<ac:parameter\s+ac:name="name">([\s\S]*?)<\/ac:parameter>/;
|
|
40
|
+
const nameMatch = inner.match(nameRe);
|
|
41
|
+
if (!nameMatch)
|
|
42
|
+
continue;
|
|
43
|
+
if (decodeHtmlCell(nameMatch[1]) !== name)
|
|
44
|
+
continue;
|
|
45
|
+
const tableMatch = inner.match(/<table[^>]*>([\s\S]*?)<\/table>/);
|
|
46
|
+
if (!tableMatch)
|
|
47
|
+
return null;
|
|
48
|
+
return parseHtmlTable(tableMatch[1]);
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Парсит Confluence storage XHTML, извлекает её таблицу и возвращает
|
|
54
|
+
* её ячейки как `string[][]` (где каждый элемент уже декодирован из HTML).
|
|
55
|
+
*/
|
|
56
|
+
export function parseHtmlTable(storage) {
|
|
57
|
+
const rows = [];
|
|
58
|
+
for (const tr of storage.matchAll(/<tr[^>]*>([\s\S]*?)<\/tr>/g)) {
|
|
59
|
+
const cells = [];
|
|
60
|
+
for (const cell of tr[1].matchAll(/<(t[hd])[^>]*>([\s\S]*?)<\/\1>/g)) {
|
|
61
|
+
cells.push(decodeHtmlCell(cell[2]));
|
|
62
|
+
}
|
|
63
|
+
if (cells.length > 0)
|
|
64
|
+
rows.push(cells);
|
|
65
|
+
}
|
|
66
|
+
return rows;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Декодирует ячейку HTML-таблицы: удаляет теги, декодирует сущности,
|
|
70
|
+
* сжимает пробелы, триммирует. Подходит для использования в парсинге
|
|
71
|
+
* storage format из Confluence.
|
|
72
|
+
*
|
|
73
|
+
* Порядок декодирования важен: `&` обрабатывается ПОСЛЕДНИМ, чтобы
|
|
74
|
+
* не разрушить уже декодированные сущности типа `<`.
|
|
75
|
+
*/
|
|
76
|
+
export function decodeHtmlCell(html) {
|
|
77
|
+
return html
|
|
78
|
+
.replace(/<br\s*\/?>/gi, ' ')
|
|
79
|
+
.replace(/<[^>]+>/g, '')
|
|
80
|
+
.replace(/ /g, ' ')
|
|
81
|
+
.replace(/</g, '<')
|
|
82
|
+
.replace(/>/g, '>')
|
|
83
|
+
.replace(/"/g, '"')
|
|
84
|
+
.replace(/'/g, "'")
|
|
85
|
+
.replace(/&/g, '&')
|
|
86
|
+
.replace(/\s+/g, ' ')
|
|
87
|
+
.trim();
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Читает таблицу из Confluence и преобразует её в типизированный массив.
|
|
91
|
+
* Первая строка таблицы считается заголовками.
|
|
92
|
+
*
|
|
93
|
+
* @param pageId ID страницы Confluence
|
|
94
|
+
* @param cfg конфиг Confluence
|
|
95
|
+
* @param mapRow функция, которая преобразует (строка: string[], заголовки: string[]) → T
|
|
96
|
+
* @param tableIndex индекс таблицы на странице (0 = первая)
|
|
97
|
+
* @returns типизированный массив строк таблицы (без заголовков)
|
|
98
|
+
* @throws если таблица пустая или содержит только заголовки
|
|
99
|
+
*/
|
|
100
|
+
export async function readAndMapTable(pageId, cfg, mapRow, tableIndex = 0) {
|
|
101
|
+
const rows = await readTableFromConfluence(pageId, cfg, tableIndex);
|
|
102
|
+
if (rows.length === 0) {
|
|
103
|
+
throw new Error(`readAndMapTable(${pageId}): table ${tableIndex} is empty`);
|
|
104
|
+
}
|
|
105
|
+
const headers = rows[0];
|
|
106
|
+
if (headers.length === 0) {
|
|
107
|
+
throw new Error(`readAndMapTable(${pageId}): table ${tableIndex} has no columns`);
|
|
108
|
+
}
|
|
109
|
+
if (rows.length === 1) {
|
|
110
|
+
console.warn(`[readAndMapTable] ${pageId}: table ${tableIndex} contains only headers, no data rows`);
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
return rows.slice(1).map((row) => mapRow(row, headers));
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Рендерит типизированный массив в Markdown-таблицу на основе описания
|
|
117
|
+
* колонок. Каждая колонка задаёт заголовок, функцию извлечения значения
|
|
118
|
+
* и опциональное выравнивание. Выравнивание транслируется markdown-it'ом
|
|
119
|
+
* в `style="text-align: right|center|left"` на <th>/<td>, что Confluence
|
|
120
|
+
* сохраняет в storage.
|
|
121
|
+
*
|
|
122
|
+
* @param items массив элементов; если пусто, возвращается сообщение «нет данных»
|
|
123
|
+
* @param columns описание колонок; не может быть пусто
|
|
124
|
+
* @throws если columns пусто
|
|
125
|
+
* @example
|
|
126
|
+
* interface Row { name: string; hours: number; }
|
|
127
|
+
* const cols: TableColumn<Row>[] = [
|
|
128
|
+
* { header: 'Имя', cell: (r) => r.name },
|
|
129
|
+
* { header: 'Часы', cell: (r) => r.hours.toFixed(2), align: 'right' },
|
|
130
|
+
* ];
|
|
131
|
+
* const md = renderMarkdownTable(rows, cols);
|
|
132
|
+
*/
|
|
133
|
+
export function renderMarkdownTable(items, columns) {
|
|
134
|
+
if (columns.length === 0) {
|
|
135
|
+
throw new Error('renderMarkdownTable: columns cannot be empty');
|
|
136
|
+
}
|
|
137
|
+
if (items.length === 0)
|
|
138
|
+
return new Markdown('_(нет данных)_');
|
|
139
|
+
const header = columns.map((c) => escapeMdTableCell(c.header));
|
|
140
|
+
const sep = columns.map((c) => alignSeparator(c.align));
|
|
141
|
+
const body = items.map((item) => columns.map((c) => escapeMdTableCell(c.cell(item))));
|
|
142
|
+
const content = [
|
|
143
|
+
`| ${header.join(' | ')} |`,
|
|
144
|
+
`| ${sep.join(' | ')} |`,
|
|
145
|
+
...body.map((r) => `| ${r.join(' | ')} |`),
|
|
146
|
+
].join('\n');
|
|
147
|
+
return new Markdown(content);
|
|
148
|
+
}
|
|
149
|
+
function alignSeparator(align) {
|
|
150
|
+
switch (align) {
|
|
151
|
+
case 'right': return '---:';
|
|
152
|
+
case 'center': return ':---:';
|
|
153
|
+
case 'left': return ':---';
|
|
154
|
+
default: return '---';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Экранирует спецсимволы для использования в ячейке Markdown-таблицы:
|
|
159
|
+
* заменяет `|` на `\|`, переносы на пробелы.
|
|
160
|
+
*/
|
|
161
|
+
export function escapeMdTableCell(s) {
|
|
162
|
+
return s.replace(/\|/g, '\\|').replace(/\n/g, ' ');
|
|
163
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ConfluenceConfig } from '../client/config.js';
|
|
2
|
+
import type { MacroRegistry } from '../macros/registry.js';
|
|
3
|
+
import { Markdown } from '../markdown/markdown.js';
|
|
4
|
+
export interface TableData {
|
|
5
|
+
name: string;
|
|
6
|
+
markdown: Markdown | string;
|
|
7
|
+
}
|
|
8
|
+
export interface PublishPageOptions {
|
|
9
|
+
/** Известный ID страницы. Альтернатива — spaceKey + title. */
|
|
10
|
+
pageId?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Space key для поиска/создания страницы по title, когда pageId неизвестен.
|
|
13
|
+
* Если страницы с таким title в space нет и createIfMissing !== false,
|
|
14
|
+
* она будет создана (опционально под parentPageId).
|
|
15
|
+
*/
|
|
16
|
+
spaceKey?: string;
|
|
17
|
+
/** Путь к markdown-файлу. Альтернатива — `markdown` со строкой/объектом. */
|
|
18
|
+
markdownPath?: string;
|
|
19
|
+
/** Готовый markdown-контент (вместо markdownPath). */
|
|
20
|
+
markdown?: Markdown | string;
|
|
21
|
+
images?: string[];
|
|
22
|
+
files?: string[];
|
|
23
|
+
tables?: TableData[];
|
|
24
|
+
/**
|
|
25
|
+
* Заголовок страницы. Для существующей страницы — переименование
|
|
26
|
+
* (по умолчанию сохраняется текущий); для поиска/создания по spaceKey —
|
|
27
|
+
* обязательный ключ поиска.
|
|
28
|
+
*/
|
|
29
|
+
title?: string;
|
|
30
|
+
/** Родитель для создаваемой страницы (только вместе со spaceKey). */
|
|
31
|
+
parentPageId?: string;
|
|
32
|
+
/** Создавать ли страницу, если по spaceKey+title не нашлась. Default: true. */
|
|
33
|
+
createIfMissing?: boolean;
|
|
34
|
+
/** Лейблы, которые нужно гарантировать на странице. */
|
|
35
|
+
labels?: string[];
|
|
36
|
+
/** Комментарий к версии страницы. */
|
|
37
|
+
versionMessage?: string;
|
|
38
|
+
/** Свой реестр макросов (default: встроенные core + table-filter). */
|
|
39
|
+
registry?: MacroRegistry;
|
|
40
|
+
/**
|
|
41
|
+
* Ключ content property для хранения content-hash.
|
|
42
|
+
* Default: 'confluence-md-sync-content-hash'.
|
|
43
|
+
*/
|
|
44
|
+
hashPropertyKey?: string;
|
|
45
|
+
/** Рендер и валидация без записи в Confluence. */
|
|
46
|
+
dryRun?: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface PublishPageResult {
|
|
49
|
+
pageId: string;
|
|
50
|
+
title: string;
|
|
51
|
+
version: number;
|
|
52
|
+
attachments: Array<{
|
|
53
|
+
filename: string;
|
|
54
|
+
id: string;
|
|
55
|
+
reused: boolean;
|
|
56
|
+
url: string;
|
|
57
|
+
}>;
|
|
58
|
+
/** false если контент совпал по hash и updatePage не вызывался. */
|
|
59
|
+
updated: boolean;
|
|
60
|
+
/** true если страница была создана в этом запуске. */
|
|
61
|
+
created: boolean;
|
|
62
|
+
/** Итоговый storage-контент (полезно в dryRun). */
|
|
63
|
+
storage: string;
|
|
64
|
+
}
|
|
65
|
+
export declare const DEFAULT_HASH_PROPERTY_KEY = "confluence-md-sync-content-hash";
|
|
66
|
+
export declare function computeContentHash(storage: string): string;
|
|
67
|
+
export declare function publishPage(opts: PublishPageOptions, cfg: ConfluenceConfig): Promise<PublishPageResult>;
|