Skip to content

Commit 7d95993

Browse files
committed
Add AI markdown authoring panel
1 parent f0ce397 commit 7d95993

22 files changed

Lines changed: 3046 additions & 111 deletions

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dc-code-paste",
3-
"version": "0.3.2",
3+
"version": "0.4.0",
44
"private": true,
55
"type": "module",
66
"scripts": {

src/lib/dc/export-document.ts

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -909,6 +909,60 @@ async function renderListItemBody(
909909
return body || " ";
910910
}
911911

912+
function renderDataTable(node: JSONContent, options: DcExportOptions): string {
913+
const palette = documentPalette(options);
914+
const isDarkEditorial = normalizeDocumentTheme(options.documentTheme) === "darkEditorial";
915+
const tableBorder = isDarkEditorial ? "#3a3a3a" : "#d8d2c4";
916+
const headerBackground = isDarkEditorial ? "#242424" : "#f3eadb";
917+
const cellBackground = isDarkEditorial ? dcDarkPageBackground : dcLightPageBackground;
918+
const tableStyle = joinStyle({
919+
width: "100%",
920+
margin: "0 0 16px",
921+
"border-collapse": "collapse",
922+
"table-layout": "fixed",
923+
color: palette.text,
924+
"font-family": safeProseFontFamily(options.bodyFontFamily),
925+
"font-size": safeBodyFontSize(options),
926+
"line-height": 1.62,
927+
});
928+
const rows = childrenOf(node)
929+
.map((row) => {
930+
const cells = childrenOf(row)
931+
.map((cell) => {
932+
const isHeader = cell.attrs?.header === true;
933+
const tag = isHeader ? "th" : "td";
934+
const cellStyle = joinStyle({
935+
padding: isHeader ? "8px 10px" : "9px 10px",
936+
border: `1px solid ${tableBorder}`,
937+
"background-color": isHeader ? headerBackground : cellBackground,
938+
color: isHeader ? palette.heading : undefined,
939+
"font-weight": isHeader ? 700 : undefined,
940+
"text-align": "left",
941+
"vertical-align": "top",
942+
"word-break": "keep-all",
943+
"overflow-wrap": "break-word",
944+
});
945+
const body = renderInlineChildren(cell, options, {
946+
text: isHeader ? palette.heading : palette.text,
947+
background: isHeader ? headerBackground : cellBackground,
948+
inlineCodeBackground: palette.inlineCodeBackground,
949+
inlineCodeText: palette.inlineCodeText,
950+
});
951+
952+
return `<${tag} style="${cellStyle}">${body || "&nbsp;"}</${tag}>`;
953+
})
954+
.join("");
955+
956+
return cells ? `<tr>${cells}</tr>` : "";
957+
})
958+
.filter(Boolean)
959+
.join("");
960+
961+
return rows
962+
? `<table width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="${palette.fallbackBackground}" style="${tableStyle}"><tbody>${rows}</tbody></table>`
963+
: "";
964+
}
965+
912966
async function renderCallout(
913967
node: JSONContent,
914968
options: DcExportOptions,
@@ -2207,12 +2261,12 @@ function comparisonBlockPalette(options: DcExportOptions) {
22072261
};
22082262
}
22092263

2210-
function tutorialStepTitle(node: JSONContent, index: number): string {
2264+
function tutorialStepTitle(node: JSONContent): string {
22112265
if (typeof node.attrs?.title === "string" && node.attrs.title.trim()) {
22122266
return node.attrs.title.trim();
22132267
}
22142268

2215-
return `단계 ${index + 1}`;
2269+
return "";
22162270
}
22172271

22182272
function tutorialStepNumber(node: JSONContent, index: number): string {
@@ -2304,16 +2358,17 @@ async function renderTutorialBlockModern(
23042358
const cards = await Promise.all(
23052359
steps.map(async (step, index) => {
23062360
const number = tutorialStepNumber(step, index);
2307-
const title = escapeHtml(tutorialStepTitle(step, index));
2361+
const title = escapeHtml(tutorialStepTitle(step));
23082362
const body = await renderTutorialStepBody(
23092363
step,
23102364
options,
23112365
palette.text,
23122366
palette.cardBackground,
23132367
);
23142368
const bodyHtml = body ? `<div style="${bodyStyle}">${body}</div>` : "";
2369+
const titleHtml = title ? `<strong style="${titleStyle}">${title}</strong>` : "";
23152370

2316-
return `<section style="${cardStyle}"><div style="${headStyle}"><span style="${numberCellStyle}"><span style="${numberStyle}">${number}</span></span><strong style="${titleStyle}">${title}</strong></div>${bodyHtml}</section>`;
2371+
return `<section style="${cardStyle}"><div style="${headStyle}"><span style="${numberCellStyle}"><span style="${numberStyle}">${number}</span></span>${titleHtml}</div>${bodyHtml}</section>`;
23172372
}),
23182373
);
23192374

@@ -2369,7 +2424,7 @@ async function renderTutorialBlockTable(
23692424
const cards = await Promise.all(
23702425
steps.map(async (step, index) => {
23712426
const number = tutorialStepNumber(step, index);
2372-
const title = escapeHtml(tutorialStepTitle(step, index));
2427+
const title = escapeHtml(tutorialStepTitle(step));
23732428
const body = await renderTutorialStepBody(
23742429
step,
23752430
options,
@@ -2388,8 +2443,9 @@ async function renderTutorialBlockTable(
23882443
const bodyRow = body
23892444
? `<tr><td style="${bodySpacerCellStyle}">&nbsp;</td><td style="${bodyCellStyle}">${body}</td></tr>`
23902445
: "";
2446+
const titleHtml = title ? `<strong style="${titleStyle}">${title}</strong>` : "&nbsp;";
23912447

2392-
return `<table width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="${palette.cardFallbackBackground}" style="${cardTableStyle}"><tbody><tr><td style="${numberCellStyle}">${badge}</td><td style="${headCellStyle}"><strong style="${titleStyle}">${title}</strong></td></tr>${bodyRow}</tbody></table>`;
2448+
return `<table width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="${palette.cardFallbackBackground}" style="${cardTableStyle}"><tbody><tr><td style="${numberCellStyle}">${badge}</td><td style="${headCellStyle}">${titleHtml}</td></tr>${bodyRow}</tbody></table>`;
23932449
}),
23942450
);
23952451

@@ -2617,6 +2673,8 @@ async function renderBlockAsync(
26172673
return renderTutorialBlock(node, options);
26182674
case "comparisonBlock":
26192675
return renderComparisonBlock(node, options);
2676+
case "dcDataTable":
2677+
return renderDataTable(node, options);
26202678
case "codeBlock":
26212679
return renderCodeBlock(node, options);
26222680
case "blockquote":

src/lib/dc/render-dc-html.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,6 @@ export function renderDcHtml(input: DcRenderInput): string {
118118
const neutralLineStyle = joinStyle({
119119
margin: input.showBackground ? "0 -16px" : undefined,
120120
padding: input.showBackground ? "0 16px" : undefined,
121-
"min-height": "1.4em",
122-
"box-sizing": "border-box",
123121
});
124122

125123
return `<div style="${neutralLineStyle}">${content}</div>`;

src/lib/editor/comparison-block.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { JSONContent } from "@tiptap/core";
2+
import { parseMarkdownInline } from "$lib/editor/markdown-inline";
23

34
type ComparisonSide = "left" | "right";
45

@@ -46,7 +47,7 @@ function createComparisonColumn(side: ComparisonSide, input: ComparisonColumnInp
4647
attrs: { side, title: input.title },
4748
content: input.lines.map((line) => ({
4849
type: "paragraph",
49-
content: [{ type: "text", text: line }],
50+
content: parseMarkdownInline(line),
5051
})),
5152
};
5253
}

src/lib/editor/draft-storage.ts

Lines changed: 195 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { JSONContent } from "@tiptap/core";
22
import type { DcDocumentTheme, DcExportStructure } from "$lib/dc/export-document";
3+
import { containsMarkdownInlineToken, parseMarkdownInline } from "$lib/editor/markdown-inline";
4+
import { parseMarkdownToDocument, sanitizeCodeHighlightLines } from "$lib/editor/markdown-import";
35
import {
46
isSupportedLanguage,
57
isSupportedTheme,
@@ -153,6 +155,198 @@ function normalizeSnapshotName(value: unknown): string | undefined {
153155
return normalized ? normalized.slice(0, 60) : undefined;
154156
}
155157

158+
function textContent(node: JSONContent): string {
159+
if (typeof node.text === "string") {
160+
return node.text;
161+
}
162+
163+
return Array.isArray(node.content) ? node.content.map(textContent).join("") : "";
164+
}
165+
166+
function plainUnmarkedTextContent(node: JSONContent): string | undefined {
167+
if (!Array.isArray(node.content) || node.content.length === 0) {
168+
return undefined;
169+
}
170+
171+
const parts: string[] = [];
172+
173+
for (const child of node.content) {
174+
if (child.type !== "text" || typeof child.text !== "string" || child.marks?.length) {
175+
return undefined;
176+
}
177+
178+
parts.push(child.text);
179+
}
180+
181+
return parts.join("");
182+
}
183+
184+
function restoreInlineMarkdownContentNode(
185+
node: JSONContent,
186+
parentType?: string,
187+
): JSONContent | undefined {
188+
const shouldRestore =
189+
node.type === "summaryItem" ||
190+
(node.type === "paragraph" &&
191+
(parentType === "comparisonColumn" || parentType === "tutorialStep"));
192+
193+
if (!shouldRestore) {
194+
return undefined;
195+
}
196+
197+
const text = plainUnmarkedTextContent(node);
198+
if (!text || !containsMarkdownInlineToken(text)) {
199+
return undefined;
200+
}
201+
202+
return {
203+
...node,
204+
content: parseMarkdownInline(text),
205+
};
206+
}
207+
208+
function restoreTutorialTitleOnlyStep(node: JSONContent): JSONContent | undefined {
209+
if (node.type !== "tutorialStep" || node.content?.length) {
210+
return undefined;
211+
}
212+
213+
const title = typeof node.attrs?.title === "string" ? node.attrs.title.trim() : "";
214+
if (!title) {
215+
return undefined;
216+
}
217+
218+
const { title: _title, ...attrs } = node.attrs ?? {};
219+
220+
return {
221+
...node,
222+
attrs,
223+
content: [
224+
{
225+
type: "paragraph",
226+
content: parseMarkdownInline(title),
227+
},
228+
],
229+
};
230+
}
231+
232+
function markdownTableRow(cells: readonly string[]): string {
233+
return `| ${cells.join(" | ")} |`;
234+
}
235+
236+
function collapsedMarkdownTableRows(text: string): string[] {
237+
const cells = text
238+
.split("|")
239+
.map((cell) => cell.trim())
240+
.filter(Boolean);
241+
const separatorStart = cells.findIndex((cell) => /^:?-{3,}:?$/.test(cell));
242+
243+
if (separatorStart < 1) {
244+
return [];
245+
}
246+
247+
let columnCount = 0;
248+
while (
249+
separatorStart + columnCount < cells.length &&
250+
/^:?-{3,}:?$/.test(cells[separatorStart + columnCount] ?? "")
251+
) {
252+
columnCount += 1;
253+
}
254+
255+
if (columnCount < 2 || separatorStart < columnCount) {
256+
return [];
257+
}
258+
259+
const header = cells.slice(separatorStart - columnCount, separatorStart);
260+
const rows = [
261+
markdownTableRow(header),
262+
markdownTableRow(cells.slice(separatorStart, separatorStart + columnCount)),
263+
];
264+
let index = separatorStart + columnCount;
265+
266+
while (index + columnCount <= cells.length) {
267+
rows.push(markdownTableRow(cells.slice(index, index + columnCount)));
268+
index += columnCount;
269+
}
270+
271+
return rows.length >= 3 ? rows : [];
272+
}
273+
274+
function restoreCollapsedMarkdownTableParagraph(node: JSONContent): JSONContent | undefined {
275+
if (node.type !== "paragraph") {
276+
return undefined;
277+
}
278+
279+
const text = textContent(node).replace(/\s+/g, " ").trim();
280+
const rows = collapsedMarkdownTableRows(text);
281+
282+
const separator = rows[1]?.replace(/\s+/g, "") ?? "";
283+
if (rows.length < 3 || !/^\|:?-{3,}:?(?:\|:?-{3,}:?)+\|$/.test(separator)) {
284+
return undefined;
285+
}
286+
287+
const table = parseMarkdownToDocument(rows.join("\n")).content?.[0];
288+
289+
return table?.type === "dcDataTable" ? table : undefined;
290+
}
291+
292+
function restoreDecorativeCodeHighlightLines(node: JSONContent): JSONContent | undefined {
293+
if (node.type !== "codeBlock" || typeof node.attrs?.highlightLines !== "string") {
294+
return undefined;
295+
}
296+
297+
const normalized = sanitizeCodeHighlightLines(
298+
node.attrs.highlightLines,
299+
textContent(node).split("\n"),
300+
);
301+
302+
if (normalized === node.attrs.highlightLines) {
303+
return undefined;
304+
}
305+
306+
const attrs = { ...node.attrs };
307+
if (normalized) {
308+
attrs.highlightLines = normalized;
309+
} else {
310+
delete attrs.highlightLines;
311+
}
312+
313+
return {
314+
...node,
315+
attrs,
316+
};
317+
}
318+
319+
function normalizeDraftDocumentNodes(node: JSONContent, parentType?: string): JSONContent {
320+
const restoredTable = restoreCollapsedMarkdownTableParagraph(node);
321+
if (restoredTable) {
322+
return restoredTable;
323+
}
324+
325+
const restoredTutorialStep = restoreTutorialTitleOnlyStep(node);
326+
if (restoredTutorialStep) {
327+
return restoredTutorialStep;
328+
}
329+
330+
const restoredInline = restoreInlineMarkdownContentNode(node, parentType);
331+
if (restoredInline) {
332+
return restoredInline;
333+
}
334+
335+
const restoredCodeHighlights = restoreDecorativeCodeHighlightLines(node);
336+
if (restoredCodeHighlights) {
337+
return restoredCodeHighlights;
338+
}
339+
340+
if (!Array.isArray(node.content)) {
341+
return node;
342+
}
343+
344+
return {
345+
...node,
346+
content: node.content.map((child) => normalizeDraftDocumentNodes(child, node.type)),
347+
};
348+
}
349+
156350
export function normalizeDraftSnapshot(value: unknown): DraftSnapshot | undefined {
157351
if (!isRecord(value)) {
158352
return undefined;
@@ -175,7 +369,7 @@ export function normalizeDraftSnapshot(value: unknown): DraftSnapshot | undefine
175369
return {
176370
version: 1,
177371
updatedAt: value.updatedAt,
178-
document: value.document,
372+
document: normalizeDraftDocumentNodes(value.document),
179373
preferences,
180374
};
181375
}

0 commit comments

Comments
 (0)