mirror of
https://github.com/docmost/docmost.git
synced 2026-05-20 00:14:10 +08:00
feat(import): add shared Confluence margin-left indent normalizer
This commit is contained in:
@@ -97,8 +97,84 @@ export function xwikiFormatter($: CheerioAPI, $root: Cheerio<any>) {
|
||||
}
|
||||
}
|
||||
|
||||
// Maximum indent level supported by the Indent editor extension (see
|
||||
// packages/editor-ext/src/lib/indent.ts). Values above this clamp down.
|
||||
const MAX_INDENT_LEVEL = 8;
|
||||
const MARGIN_LEFT_RE = /margin-left\s*:\s*(-?\d*\.?\d+)\s*px/i;
|
||||
const MARGIN_LEFT_STRIP_RE = /margin-left\s*:\s*-?\d*\.?\d+\s*px\s*;?/i;
|
||||
|
||||
/**
|
||||
* Confluence encodes paragraph indent as inline `style="margin-left: Npx"`.
|
||||
* The per-level pixel value differs by edition: Cloud uses 30 (max 6 levels),
|
||||
* Data Center uses 40 (no upper limit). The HTML-export ZIP path has no
|
||||
* edition information available, so we auto-detect the per-level unit from
|
||||
* the GCD of all margin-left values in the document. The API converter can
|
||||
* pass `pxPerLevel` explicitly when the edition is known.
|
||||
*
|
||||
* Levels are written to `data-indent` for the TipTap Indent extension to
|
||||
* pick up; the margin-left style is stripped from the element so the
|
||||
* normalized indent doesn't double up with the editor's own indent padding.
|
||||
*/
|
||||
export function applyConfluenceMarginLeftIndent(
|
||||
$: CheerioAPI,
|
||||
$root: Cheerio<any>,
|
||||
options?: { pxPerLevel?: number },
|
||||
): void {
|
||||
const $els = $root.find('p, h1, h2, h3, h4, h5, h6');
|
||||
|
||||
const values: number[] = [];
|
||||
$els.each((_, el) => {
|
||||
const style = $(el).attr('style');
|
||||
if (!style) return;
|
||||
const match = MARGIN_LEFT_RE.exec(style);
|
||||
if (!match) return;
|
||||
const px = parseFloat(match[1]);
|
||||
if (Number.isFinite(px) && px > 0) values.push(px);
|
||||
});
|
||||
if (values.length === 0) return;
|
||||
|
||||
const unit = options?.pxPerLevel ?? detectIndentUnit(values);
|
||||
if (!unit || unit <= 0) return;
|
||||
|
||||
$els.each((_, el) => {
|
||||
const $el = $(el);
|
||||
const style = $el.attr('style');
|
||||
if (!style) return;
|
||||
const match = MARGIN_LEFT_RE.exec(style);
|
||||
if (!match) return;
|
||||
const px = parseFloat(match[1]);
|
||||
if (!Number.isFinite(px) || px <= 0) return;
|
||||
const level = Math.min(
|
||||
MAX_INDENT_LEVEL,
|
||||
Math.max(1, Math.round(px / unit)),
|
||||
);
|
||||
$el.attr('data-indent', String(level));
|
||||
const remaining = style.replace(MARGIN_LEFT_STRIP_RE, '').trim();
|
||||
if (remaining) {
|
||||
$el.attr('style', remaining);
|
||||
} else {
|
||||
$el.removeAttr('style');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function detectIndentUnit(values: number[]): number {
|
||||
// Confluence emits floats like "30.0"; round to ints for a clean GCD.
|
||||
const ints = values.map((v) => Math.round(v)).filter((v) => v > 0);
|
||||
if (ints.length === 0) return 0;
|
||||
return ints.reduce((a, b) => gcd(a, b));
|
||||
}
|
||||
|
||||
function gcd(a: number, b: number): number {
|
||||
while (b !== 0) {
|
||||
[a, b] = [b, a % b];
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
export function defaultHtmlFormatter($: CheerioAPI, $root: Cheerio<any>) {
|
||||
normalizeTableColumnWidths($, $root);
|
||||
applyConfluenceMarginLeftIndent($, $root);
|
||||
|
||||
$root.find('a[href]').each((_, el) => {
|
||||
const $el = $(el);
|
||||
|
||||
Reference in New Issue
Block a user