Skip to content

Commit 71e2a83

Browse files
committed
fix: preserve DC export prose font styles
1 parent f9da8ff commit 71e2a83

4 files changed

Lines changed: 60 additions & 32 deletions

File tree

src/lib/dc/export-document.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ function removeStyleDeclaration(style: string, property: string, value: string):
458458
.join(";");
459459
}
460460

461-
function canCompactInheritedFontSize(tagName: string): boolean {
461+
function canCompactInheritedProseStyle(tagName: string): boolean {
462462
return tagName === "table" || tagName === "div" || tagName === "section";
463463
}
464464

@@ -473,19 +473,24 @@ function compactInheritedProseStyles(html: string, options: DcExportOptions): st
473473
(_match, tagName: string, beforeStyle: string, rawStyle: string) => {
474474
let style = rawStyle;
475475
const normalizedTagName = tagName.toLowerCase();
476+
const preservesCellStyle = normalizedTagName === "td" || normalizedTagName === "th";
477+
const isCompactableWrapper = canCompactInheritedProseStyle(normalizedTagName);
476478

477479
if (styleHasDeclaration(style, "font-family", inheritedFontFamily)) {
478-
if (keptInheritedFontFamilyCount >= 2) {
480+
if (!preservesCellStyle && keptInheritedFontFamilyCount >= 2) {
479481
style = removeStyleDeclaration(style, "font-family", inheritedFontFamily);
480-
} else {
482+
} else if (
483+
keptInheritedFontFamilyCount < 2 &&
484+
(preservesCellStyle || isCompactableWrapper)
485+
) {
481486
keptInheritedFontFamilyCount += 1;
482487
}
483488
}
484489

485490
if (styleHasDeclaration(style, "font-size", inheritedFontSize)) {
486-
if (keptInheritedFontSizeCount >= 2) {
491+
if (!preservesCellStyle && keptInheritedFontSizeCount >= 2) {
487492
style = removeStyleDeclaration(style, "font-size", inheritedFontSize);
488-
} else if (canCompactInheritedFontSize(normalizedTagName)) {
493+
} else if (keptInheritedFontSizeCount < 2 && (preservesCellStyle || isCompactableWrapper)) {
489494
keptInheritedFontSizeCount += 1;
490495
}
491496
}
@@ -521,6 +526,7 @@ function renderAttributionFooter(options: DcExportOptions): string {
521526

522527
function renderDcTableBlock({
523528
body,
529+
options,
524530
backgroundColor,
525531
fallbackBackground,
526532
borderColor,
@@ -530,6 +536,7 @@ function renderDcTableBlock({
530536
padding = "12px 14px",
531537
}: {
532538
body: string;
539+
options: DcExportOptions;
533540
backgroundColor: string;
534541
fallbackBackground: string;
535542
borderColor?: string;
@@ -551,6 +558,7 @@ function renderDcTableBlock({
551558
const cellStyle = joinStyle({
552559
padding,
553560
"background-color": backgroundColor,
561+
"font-size": safeBodyFontSize(options),
554562
});
555563

556564
return `<table width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="${fallbackBackground}" style="${tableStyle}"><tbody><tr><td style="${cellStyle}">${body}</td></tr></tbody></table>`;
@@ -825,7 +833,6 @@ async function renderList(
825833
color: palette.text,
826834
"font-family": proseFontFamily,
827835
"font-size": bodyFontSize,
828-
"font-weight": 400,
829836
"line-height": 1.7,
830837
"text-align": ordered ? "right" : "center",
831838
"vertical-align": "top",
@@ -835,7 +842,6 @@ async function renderList(
835842
color: palette.text,
836843
"font-family": proseFontFamily,
837844
"font-size": bodyFontSize,
838-
"font-weight": 400,
839845
"line-height": 1.7,
840846
"white-space": "nowrap",
841847
});
@@ -844,15 +850,13 @@ async function renderList(
844850
color: palette.text,
845851
"font-family": proseFontFamily,
846852
"font-size": bodyFontSize,
847-
"font-weight": 400,
848853
"line-height": 1.7,
849854
"vertical-align": "top",
850855
});
851856
const bodyTextStyle = joinStyle({
852857
color: palette.text,
853858
"font-family": proseFontFamily,
854859
"font-size": bodyFontSize,
855-
"font-weight": 400,
856860
"line-height": 1.7,
857861
});
858862
const rows = await Promise.all(
@@ -1007,6 +1011,7 @@ async function renderCallout(
10071011
if (isDcTableStructure(options)) {
10081012
return renderDcTableBlock({
10091013
body: content,
1014+
options,
10101015
backgroundColor: palette.background,
10111016
fallbackBackground: calloutFallbackBackground(kind, options, node),
10121017
borderColor: palette.border,
@@ -1094,6 +1099,7 @@ async function renderLinkBox(node: JSONContent, options: DcExportOptions): Promi
10941099
if (isDcTableStructure(options)) {
10951100
return renderDcTableBlock({
10961101
body: content,
1102+
options,
10971103
backgroundColor: boxBackground,
10981104
fallbackBackground: isDarkEditorial ? "#0b0b0b" : dcTableFallbackColors.referenceBackground,
10991105
border: `1px solid ${boxBorder}`,
@@ -1159,6 +1165,7 @@ async function renderBlockquote(node: JSONContent, options: DcExportOptions): Pr
11591165
if (isDcTableStructure(options)) {
11601166
return renderDcTableBlock({
11611167
body: content,
1168+
options,
11621169
backgroundColor: quoteBackground(quoteStyle, palette),
11631170
fallbackBackground: quoteFallbackBackground(options),
11641171
border: quoteStyle === "pull" ? undefined : quoteTableBorder(quoteStyle, palette),
@@ -1361,6 +1368,7 @@ async function renderSectionHeading(node: JSONContent, options: DcExportOptions)
13611368
if (isDcTableStructure(options)) {
13621369
return renderDcTableBlock({
13631370
body,
1371+
options,
13641372
backgroundColor: palette.articleBackground,
13651373
fallbackBackground: palette.fallbackBackground,
13661374
borderColor: palette.sectionAccent,

src/lib/dc/font-stacks.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ function hasAnySignal(fonts: readonly string[], signals: ReadonlySet<string>): b
132132
function appendFallbackFonts(
133133
primaryFonts: readonly string[],
134134
fallbackFonts: readonly string[],
135+
separator = ", ",
135136
): string {
136137
const seen = new Set<string>();
137138
const stack: string[] = [];
@@ -158,7 +159,7 @@ function appendFallbackFonts(
158159
stack.push(font);
159160
}
160161

161-
return stack.join(", ");
162+
return stack.join(separator);
162163
}
163164

164165
function fallbackFontsFor(
@@ -181,7 +182,7 @@ function compactProseFallbackFonts(primaryFonts: readonly string[]): readonly st
181182
return ["Batang", "serif"];
182183
}
183184

184-
return ["Malgun Gothic", "맑은 고딕", "sans-serif"];
185+
return ["sans-serif"];
185186
}
186187

187188
export function buildFontStack(
@@ -212,15 +213,19 @@ export function safeDcProseFontFamily(value = ""): string {
212213
const primaryFonts = parseFontFamily(value);
213214
const selectedFonts = primaryFonts.length > 0 ? primaryFonts : ["Malgun Gothic"];
214215

215-
return appendFallbackFonts(selectedFonts.slice(0, 1), compactProseFallbackFonts(selectedFonts));
216+
return appendFallbackFonts(
217+
selectedFonts.slice(0, 1),
218+
compactProseFallbackFonts(selectedFonts),
219+
",",
220+
);
216221
}
217222

218223
export function safeDcCodeFontFamily(): string {
219-
return appendFallbackFonts([], ["Cascadia Mono", "Pretendard", "D2Coding", "monospace"]);
224+
return appendFallbackFonts([], ["Cascadia Mono", "D2Coding", "monospace"], ",");
220225
}
221226

222227
export function safeDcInlineCodeFontFamily(): string {
223-
return appendFallbackFonts([], ["Pretendard", "Cascadia Mono", "D2Coding", "monospace"]);
228+
return appendFallbackFonts([], ["Pretendard", "monospace"], ",");
224229
}
225230

226231
export const defaultProseFontFamily = safeProseFontFamily();

tests/unit/export-document.test.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,29 @@ describe("exportDocumentToDcHtml", () => {
8181
expect(html).toContain("백그라운드 소모 감소");
8282
});
8383

84+
it("keeps prose font styles after markdown-authored data tables", async () => {
85+
const document = parseMarkdownToDocument(
86+
[
87+
"| 함수 | 예시 | 결과 |",
88+
"| --- | --- | --- |",
89+
"| gcd | `gcd(24, 60)` | 12 |",
90+
"| lcm | `lcm(8, 10)` | 40 |",
91+
"",
92+
"`map` 노드 조작도 이제 재할당 없이 가능하다.",
93+
].join("\n"),
94+
);
95+
96+
const html = await exportDocumentToDcHtml(document, exportOptions);
97+
const afterTableHtml = html.slice(Math.max(0, html.indexOf("map") - 240));
98+
99+
expect(afterTableHtml).toContain(
100+
`font-family:${expectedDefaultDcProseFontFamily};font-size:17px`,
101+
);
102+
expect(afterTableHtml).toMatch(
103+
/<td style="padding:0;color:#[0-9a-f]{6};font-family:[^"]+;font-size:17px;line-height:1\.72">/,
104+
);
105+
});
106+
84107
it("appends a subtle linked attribution footer to copied DC HTML", async () => {
85108
const document: JSONContent = {
86109
type: "doc",
@@ -289,10 +312,10 @@ describe("exportDocumentToDcHtml", () => {
289312
expect(html).toContain("&bull;</span></td><td");
290313
expect(html).toContain(">1.</span></td><td");
291314
expect(html).toMatch(
292-
/<span style="color:#[0-9a-f]{6};font-size:17px;font-weight:400;line-height:1\.7"> <\/span>/,
315+
/<td style="padding:0 0 7px;color:#[0-9a-f]{6};font-family:[^"]+;font-size:17px;line-height:1\.7;vertical-align:top"><span style="color:#[0-9a-f]{6};line-height:1\.7"> <\/span><\/td>/,
293316
);
294317
expect(html).toMatch(
295-
/<span style="color:#[0-9a-f]{6};font-size:17px;font-weight:400;line-height:1\.7"> <\/span>/,
318+
/<td style="padding:0 0 7px;color:#[0-9a-f]{6};font-family:[^"]+;font-size:17px;line-height:1\.7;vertical-align:top"><span style="color:#[0-9a-f]{6};line-height:1\.7"> <\/span><\/td>/,
296319
);
297320
expect(html).toContain("<hr");
298321
expect(html).toContain("const");
@@ -438,16 +461,12 @@ describe("exportDocumentToDcHtml", () => {
438461
`<span style="font-family:${safeDcProseFontFamily("Georgia, Times New Roman, serif")};font-size:18px">선택 스타일</span>`,
439462
);
440463
expect(html).toContain(`font-family:${expectedDefaultDcProseFontFamily}`);
441-
expect(
442-
html.match(new RegExp(`font-family:${escapeRegExp(expectedDefaultDcProseFontFamily)}`, "g"))
443-
?.length ?? 0,
444-
).toBeLessThanOrEqual(2);
445464
expect(html).toContain("font-size:17px");
446465
expect(html).toContain(`<td style="padding:18px;background-color:`);
447466
expect(html).toContain(`font-family:${expectedDefaultDcProseFontFamily};font-size:17px`);
448467
expect(html).toMatch(
449468
new RegExp(
450-
`<table width="100%"[^>]*style="width:100%;margin:0 0 14px;border-collapse:collapse"><tbody><tr><td style="padding:0;color:#[0-9a-f]{6};font-size:17px;line-height:1\\.72">`,
469+
`<table width="100%"[^>]*style="width:100%;margin:0 0 14px;border-collapse:collapse"><tbody><tr><td style="padding:0;color:#[0-9a-f]{6};font-family:${escapeRegExp(expectedDefaultDcProseFontFamily)};font-size:17px;line-height:1\\.72">`,
451470
),
452471
);
453472
});
@@ -715,7 +734,7 @@ describe("exportDocumentToDcHtml", () => {
715734
expect(
716735
html.match(new RegExp(`font-family:${escapeRegExp(expectedDefaultDcProseFontFamily)}`, "g"))
717736
?.length ?? 0,
718-
).toBeLessThanOrEqual(2);
737+
).toBeLessThan(50);
719738
expect(html).toMatch(
720739
/<strong style="color:#[0-9a-f]{6};font-size:20px;font-weight:700;line-height:1\.35"> <\/strong>/,
721740
);

tests/unit/font-stacks.test.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,11 @@ describe("font stacks", () => {
113113
});
114114

115115
it("keeps DC export font stacks compact enough for inline HTML", () => {
116-
expect(safeDcProseFontFamily(safeProseFontFamily("Inter"))).toBe(
117-
"Inter, Malgun Gothic, 맑은 고딕, sans-serif",
118-
);
119-
expect(safeDcProseFontFamily()).toBe("Malgun Gothic, 맑은 고딕, sans-serif");
120-
expect(safeDcProseFontFamily("Georgia, Times New Roman, serif")).toBe("Georgia, Batang, serif");
121-
expect(safeDcCodeFontFamily()).toBe("Cascadia Mono, Pretendard, D2Coding, monospace");
122-
expect(safeDcInlineCodeFontFamily()).toBe("Pretendard, Cascadia Mono, D2Coding, monospace");
116+
expect(safeDcProseFontFamily(safeProseFontFamily("Inter"))).toBe("Inter,sans-serif");
117+
expect(safeDcProseFontFamily()).toBe("Malgun Gothic,sans-serif");
118+
expect(safeDcProseFontFamily("Georgia, Times New Roman, serif")).toBe("Georgia,Batang,serif");
119+
expect(safeDcCodeFontFamily()).toBe("Cascadia Mono,D2Coding,monospace");
120+
expect(safeDcInlineCodeFontFamily()).toBe("Pretendard,monospace");
123121
});
124122

125123
it("strips unsafe font-family punctuation before export", () => {
@@ -130,8 +128,6 @@ describe("font stacks", () => {
130128
expect(stack).not.toContain(";");
131129
expect(stack).not.toContain(":");
132130

133-
expect(safeDcProseFontFamily('Pretendard";color:red')).toBe(
134-
"Pretendardcolorred, Malgun Gothic, 맑은 고딕, sans-serif",
135-
);
131+
expect(safeDcProseFontFamily('Pretendard";color:red')).toBe("Pretendardcolorred,sans-serif");
136132
});
137133
});

0 commit comments

Comments
 (0)