测试
This commit is contained in:
Binary file not shown.
@@ -2,6 +2,7 @@
|
|||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
import type { Prisma } from '@prisma/client';
|
import type { Prisma } from '@prisma/client';
|
||||||
import { requireApiAuth } from '@/lib/api-auth';
|
import { requireApiAuth } from '@/lib/api-auth';
|
||||||
|
import { cleanupAccidentalStandaloneInlineCode, markdownToImportHtml } from '@/lib/markdown-import';
|
||||||
|
|
||||||
type PageRef = { id: string; parentId: string | null };
|
type PageRef = { id: string; parentId: string | null };
|
||||||
|
|
||||||
@@ -119,11 +120,23 @@ export async function PUT(
|
|||||||
updateData.title = trimmed || '无标题';
|
updateData.title = trimmed || '无标题';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (body.content !== undefined) {
|
if (body.content !== undefined || body.importMarkdown !== undefined) {
|
||||||
if (typeof body.content !== 'string') {
|
if (body.content !== undefined && typeof body.content !== 'string') {
|
||||||
return NextResponse.json({ error: 'Invalid content' }, { status: 400 });
|
return NextResponse.json({ error: 'Invalid content' }, { status: 400 });
|
||||||
}
|
}
|
||||||
updateData.content = body.content;
|
if (body.importMarkdown !== undefined && typeof body.importMarkdown !== 'string') {
|
||||||
|
return NextResponse.json({ error: 'Invalid importMarkdown' }, { status: 400 });
|
||||||
|
}
|
||||||
|
const source = typeof body.importMarkdown === 'string' ? markdownToImportHtml(body.importMarkdown) : (body.content as string);
|
||||||
|
updateData.content = cleanupAccidentalStandaloneInlineCode(source);
|
||||||
|
if (typeof body.importMarkdown === 'string' && process.env.NODE_ENV !== 'production') {
|
||||||
|
const lines = body.importMarkdown.replace(/\r\n?/g, '\n').split('\n');
|
||||||
|
const fenceLines = lines
|
||||||
|
.map((line: string, idx: number) => (line.trimStart().startsWith('```') || line.trimStart().startsWith('~~~') ? idx + 1 : -1))
|
||||||
|
.filter((n: number) => n > 0);
|
||||||
|
const preEnd = String(updateData.content || '').indexOf('</code></pre>');
|
||||||
|
console.info(`[import-md][PUT] id=${id} fences=${fenceLines.join(',') || 'none'} preEnd=${preEnd} len=${String(updateData.content || '').length}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (body.icon !== undefined) {
|
if (body.icon !== undefined) {
|
||||||
@@ -213,3 +226,5 @@ export async function DELETE(
|
|||||||
return NextResponse.json({ error: 'Error deleting page' }, { status: 500 });
|
return NextResponse.json({ error: 'Error deleting page' }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
import { requireApiAuth } from '@/lib/api-auth';
|
import { requireApiAuth } from '@/lib/api-auth';
|
||||||
|
import { cleanupAccidentalStandaloneInlineCode, markdownToImportHtml } from '@/lib/markdown-import';
|
||||||
|
|
||||||
const MAX_TITLE_LENGTH = 200;
|
const MAX_TITLE_LENGTH = 200;
|
||||||
const MAX_TAGS = 20;
|
const MAX_TAGS = 20;
|
||||||
@@ -64,7 +65,19 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const type = body.type === 'folder' ? 'folder' : 'file';
|
const type = body.type === 'folder' ? 'folder' : 'file';
|
||||||
const content = typeof body.content === 'string' ? body.content : '';
|
const importMarkdown = typeof body.importMarkdown === 'string' ? body.importMarkdown : null;
|
||||||
|
const rawContent = typeof body.content === 'string' ? body.content : '';
|
||||||
|
const content = cleanupAccidentalStandaloneInlineCode(
|
||||||
|
importMarkdown !== null ? markdownToImportHtml(importMarkdown) : rawContent
|
||||||
|
);
|
||||||
|
if (importMarkdown !== null && process.env.NODE_ENV !== 'production') {
|
||||||
|
const lines = importMarkdown.replace(/\r\n?/g, '\n').split('\n');
|
||||||
|
const fenceLines = lines
|
||||||
|
.map((line: string, idx: number) => (line.trimStart().startsWith('```') || line.trimStart().startsWith('~~~') ? idx + 1 : -1))
|
||||||
|
.filter((n: number) => n > 0);
|
||||||
|
const preEnd = content.indexOf('</code></pre>');
|
||||||
|
console.info(`[import-md][POST] title="${title}" fences=${fenceLines.join(',') || 'none'} preEnd=${preEnd} len=${content.length}`);
|
||||||
|
}
|
||||||
const parentId = typeof body.parentId === 'string' ? body.parentId : null;
|
const parentId = typeof body.parentId === 'string' ? body.parentId : null;
|
||||||
const tags = normalizeTags(body.tags);
|
const tags = normalizeTags(body.tags);
|
||||||
const icon = typeof body.icon === 'string' ? body.icon : null;
|
const icon = typeof body.icon === 'string' ? body.icon : null;
|
||||||
@@ -105,3 +118,6 @@ export async function POST(request: Request) {
|
|||||||
return NextResponse.json({ error: 'Error creating page' }, { status: 500 });
|
return NextResponse.json({ error: 'Error creating page' }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import JSZip from "jszip";
|
import JSZip from "jszip";
|
||||||
import { marked } from "marked";
|
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { requireApiAuth } from "@/lib/api-auth";
|
import { requireApiAuth } from "@/lib/api-auth";
|
||||||
|
import { markdownToImportHtml } from "@/lib/markdown-import";
|
||||||
|
|
||||||
type ZipEntry = {
|
type ZipEntry = {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -68,7 +68,7 @@ async function parseMarkdownWithFrontmatter(filePath: string, content: string):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const htmlContent = await marked(markdownBody, { gfm: true, breaks: true });
|
const htmlContent = markdownToImportHtml(markdownBody);
|
||||||
return { title, order, tags, htmlContent };
|
return { title, order, tags, htmlContent };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import { Markdown } from "tiptap-markdown";
|
|||||||
import { Fragment, type Node as ProseMirrorNode, type Schema } from "@tiptap/pm/model";
|
import { Fragment, type Node as ProseMirrorNode, type Schema } from "@tiptap/pm/model";
|
||||||
import { TextSelection } from "@tiptap/pm/state";
|
import { TextSelection } from "@tiptap/pm/state";
|
||||||
import { buildDropIndicatorText, mergeBlockTexts, moveContiguousSpan } from "@/lib/editor-block-ops";
|
import { buildDropIndicatorText, mergeBlockTexts, moveContiguousSpan } from "@/lib/editor-block-ops";
|
||||||
|
import { cleanupAccidentalStandaloneInlineCode } from "@/lib/markdown-import";
|
||||||
|
|
||||||
interface EditorProps {
|
interface EditorProps {
|
||||||
content: string;
|
content: string;
|
||||||
@@ -617,6 +618,8 @@ export function Editor({ content, onChange, onEditorReady, onToggleAI, onExport,
|
|||||||
targetInsertionIndex: number;
|
targetInsertionIndex: number;
|
||||||
}>({ sourceStartIndex: -1, sourceEndIndex: -1, targetInsertionIndex: -1 });
|
}>({ sourceStartIndex: -1, sourceEndIndex: -1, targetInsertionIndex: -1 });
|
||||||
const activeBlockIndexRef = useRef<number | null>(null);
|
const activeBlockIndexRef = useRef<number | null>(null);
|
||||||
|
const suppressNextUpdateRef = useRef(false);
|
||||||
|
const allowOnUpdateRef = useRef(true);
|
||||||
const { fontFamily, fontSize, lineHeight, tableLineHeight } = useSettingsStore();
|
const { fontFamily, fontSize, lineHeight, tableLineHeight } = useSettingsStore();
|
||||||
|
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
@@ -691,6 +694,11 @@ export function Editor({ content, onChange, onEditorReady, onToggleAI, onExport,
|
|||||||
],
|
],
|
||||||
content,
|
content,
|
||||||
onUpdate: ({ editor }) => {
|
onUpdate: ({ editor }) => {
|
||||||
|
if (suppressNextUpdateRef.current) {
|
||||||
|
suppressNextUpdateRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!allowOnUpdateRef.current) return;
|
||||||
if (editor.getHTML() !== content) {
|
if (editor.getHTML() !== content) {
|
||||||
onChange(editor.getHTML());
|
onChange(editor.getHTML());
|
||||||
}
|
}
|
||||||
@@ -702,6 +710,7 @@ export function Editor({ content, onChange, onEditorReady, onToggleAI, onExport,
|
|||||||
spellcheck: "false",
|
spellcheck: "false",
|
||||||
},
|
},
|
||||||
handleKeyDown: (_view, event) => {
|
handleKeyDown: (_view, event) => {
|
||||||
|
allowOnUpdateRef.current = true;
|
||||||
if (!editable) return false;
|
if (!editable) return false;
|
||||||
|
|
||||||
const editorInstance = editor;
|
const editorInstance = editor;
|
||||||
@@ -727,6 +736,7 @@ export function Editor({ content, onChange, onEditorReady, onToggleAI, onExport,
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
handleClick: (view, pos, event) => {
|
handleClick: (view, pos, event) => {
|
||||||
|
allowOnUpdateRef.current = true;
|
||||||
if (!onOpenWikiLink) return false;
|
if (!onOpenWikiLink) return false;
|
||||||
const pointerEvent = event as MouseEvent;
|
const pointerEvent = event as MouseEvent;
|
||||||
if (!pointerEvent.metaKey && !pointerEvent.ctrlKey) return false;
|
if (!pointerEvent.metaKey && !pointerEvent.ctrlKey) return false;
|
||||||
@@ -738,6 +748,20 @@ export function Editor({ content, onChange, onEditorReady, onToggleAI, onExport,
|
|||||||
onOpenWikiLink(title);
|
onOpenWikiLink(title);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
handleDOMEvents: {
|
||||||
|
mousedown: () => {
|
||||||
|
allowOnUpdateRef.current = true;
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
paste: () => {
|
||||||
|
allowOnUpdateRef.current = true;
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
beforeinput: () => {
|
||||||
|
allowOnUpdateRef.current = true;
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
editable,
|
editable,
|
||||||
immediatelyRender: false,
|
immediatelyRender: false,
|
||||||
@@ -882,9 +906,15 @@ export function Editor({ content, onChange, onEditorReady, onToggleAI, onExport,
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editor && content !== editor.getHTML()) {
|
if (editor && content !== editor.getHTML()) {
|
||||||
|
const normalizedContent = cleanupAccidentalStandaloneInlineCode(content);
|
||||||
|
suppressNextUpdateRef.current = true;
|
||||||
|
allowOnUpdateRef.current = false;
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
editor.commands.setContent(content);
|
editor.commands.setContent(normalizedContent);
|
||||||
});
|
});
|
||||||
|
if (normalizedContent !== content) {
|
||||||
|
onChange(normalizedContent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (editor && onEditorReady) {
|
if (editor && onEditorReady) {
|
||||||
onEditorReady(editor);
|
onEditorReady(editor);
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
import React, { createContext, useContext, useRef, useState, useCallback } from "react";
|
import React, { createContext, useContext, useRef, useState, useCallback } from "react";
|
||||||
import { useEditorStore } from "@/lib/store";
|
import { useEditorStore } from "@/lib/store";
|
||||||
import { marked } from "marked";
|
|
||||||
import JSZip from "jszip";
|
import JSZip from "jszip";
|
||||||
|
import { markdownToImportHtml } from "@/lib/markdown-import";
|
||||||
|
|
||||||
interface ImportContextType {
|
interface ImportContextType {
|
||||||
triggerImport: (parentId?: string | null) => void;
|
triggerImport: (parentId?: string | null) => void;
|
||||||
@@ -27,13 +27,14 @@ export function ImportProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const [isImporting, setIsImporting] = useState(false);
|
const [isImporting, setIsImporting] = useState(false);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const targetParentIdRef = useRef<string | null>(null);
|
const targetParentIdRef = useRef<string | null>(null);
|
||||||
const { fetchPages } = useEditorStore();
|
const { pages, fetchPages } = useEditorStore();
|
||||||
|
|
||||||
const postPage = useCallback(
|
const postPage = useCallback(
|
||||||
async (
|
async (
|
||||||
payload: {
|
payload: {
|
||||||
title: string;
|
title: string;
|
||||||
content?: string;
|
content?: string;
|
||||||
|
importMarkdown?: string;
|
||||||
parentId?: string | null;
|
parentId?: string | null;
|
||||||
type: "file" | "folder";
|
type: "file" | "folder";
|
||||||
},
|
},
|
||||||
@@ -62,6 +63,41 @@ export function ImportProvider({ children }: { children: React.ReactNode }) {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const putPage = useCallback(
|
||||||
|
async (
|
||||||
|
id: string,
|
||||||
|
payload: {
|
||||||
|
title?: string;
|
||||||
|
content?: string;
|
||||||
|
importMarkdown?: string;
|
||||||
|
parentId?: string | null;
|
||||||
|
type?: "file" | "folder";
|
||||||
|
},
|
||||||
|
contextLabel: string
|
||||||
|
) => {
|
||||||
|
const res = await fetch(`/api/pages/${id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
credentials: "same-origin",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
let detail = "";
|
||||||
|
try {
|
||||||
|
const data = await res.json();
|
||||||
|
detail = typeof data?.error === "string" ? data.error : JSON.stringify(data);
|
||||||
|
} catch {
|
||||||
|
detail = await res.text();
|
||||||
|
}
|
||||||
|
throw new ImportApiError(`${contextLabel}失败(${res.status})${detail ? `: ${detail}` : ""}`, res.status, detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const triggerImport = useCallback((parentId: string | null = null) => {
|
const triggerImport = useCallback((parentId: string | null = null) => {
|
||||||
targetParentIdRef.current = parentId;
|
targetParentIdRef.current = parentId;
|
||||||
if (fileInputRef.current) {
|
if (fileInputRef.current) {
|
||||||
@@ -94,13 +130,30 @@ export function ImportProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const handleSingleFileImport = async (file: File) => {
|
const handleSingleFileImport = async (file: File) => {
|
||||||
const text = await file.text();
|
const text = await file.text();
|
||||||
const title = file.name.replace(/\.md$/i, "");
|
const title = file.name.replace(/\.md$/i, "");
|
||||||
const html = await marked.parse(text);
|
const html = markdownToImportHtml(text);
|
||||||
|
const parentId = targetParentIdRef.current;
|
||||||
|
const existing = pages.find((p) => p.type === "file" && p.parentId === parentId && (p.title || "").trim() === title.trim());
|
||||||
|
if (existing) {
|
||||||
|
await putPage(
|
||||||
|
existing.id,
|
||||||
|
{
|
||||||
|
title,
|
||||||
|
content: html,
|
||||||
|
importMarkdown: text,
|
||||||
|
parentId,
|
||||||
|
type: "file",
|
||||||
|
},
|
||||||
|
"更新导入文件"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await postPage(
|
await postPage(
|
||||||
{
|
{
|
||||||
title,
|
title,
|
||||||
content: html,
|
content: html,
|
||||||
parentId: targetParentIdRef.current,
|
importMarkdown: text,
|
||||||
|
parentId,
|
||||||
type: "file",
|
type: "file",
|
||||||
},
|
},
|
||||||
"创建导入文件"
|
"创建导入文件"
|
||||||
@@ -199,17 +252,32 @@ export function ImportProvider({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
const markdown = await zipFile.async("string");
|
const markdown = await zipFile.async("string");
|
||||||
const title = fileName.replace(/\.md$/i, "").replace(/\.txt$/i, "");
|
const title = fileName.replace(/\.md$/i, "").replace(/\.txt$/i, "");
|
||||||
const html = await marked.parse(markdown);
|
const html = markdownToImportHtml(markdown);
|
||||||
|
const existing = pages.find((p) => p.type === "file" && p.parentId === parentId && (p.title || "").trim() === title.trim());
|
||||||
await postPage(
|
if (existing) {
|
||||||
{
|
await putPage(
|
||||||
title,
|
existing.id,
|
||||||
content: html,
|
{
|
||||||
parentId,
|
title,
|
||||||
type: "file",
|
content: html,
|
||||||
},
|
importMarkdown: markdown,
|
||||||
`创建文件 ${path}`
|
parentId,
|
||||||
);
|
type: "file",
|
||||||
|
},
|
||||||
|
`更新文件 ${path}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await postPage(
|
||||||
|
{
|
||||||
|
title,
|
||||||
|
content: html,
|
||||||
|
importMarkdown: markdown,
|
||||||
|
parentId,
|
||||||
|
type: "file",
|
||||||
|
},
|
||||||
|
`创建文件 ${path}`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import { marked } from "marked";
|
||||||
|
|
||||||
|
type MarkdownToHtmlOptions = {
|
||||||
|
breaks?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CodeFenceBlock = {
|
||||||
|
placeholder: string;
|
||||||
|
html: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function escapeHtml(raw: string): string {
|
||||||
|
return raw
|
||||||
|
.replaceAll("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">")
|
||||||
|
.replaceAll('"', """)
|
||||||
|
.replaceAll("'", "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeRegExp(raw: string): string {
|
||||||
|
return raw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCodeBlockHtml(langRaw: string, codeRaw: string): string {
|
||||||
|
const language = (langRaw || "").trim().split(/\s+/)[0];
|
||||||
|
const classAttr = language ? ` class="language-${escapeHtml(language)}"` : "";
|
||||||
|
return `<pre><code${classAttr}>${escapeHtml(codeRaw)}</code></pre>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseWithMarked(markdown: string, breaks: boolean): string {
|
||||||
|
return marked.parse(markdown, { gfm: true, breaks }) as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scanAndMaskFencedCodeBlocks(markdown: string): { masked: string; blocks: CodeFenceBlock[] } {
|
||||||
|
const normalized = markdown.replace(/\r\n?/g, "\n");
|
||||||
|
const lines = normalized.split("\n");
|
||||||
|
const output: string[] = [];
|
||||||
|
const blocks: CodeFenceBlock[] = [];
|
||||||
|
let blockIndex = 0;
|
||||||
|
|
||||||
|
let inFence = false;
|
||||||
|
let fenceChar = "";
|
||||||
|
let fenceLength = 0;
|
||||||
|
let fenceInfo = "";
|
||||||
|
let placeholder = "";
|
||||||
|
let contentLines: string[] = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!inFence) {
|
||||||
|
const open = line.match(/^ {0,3}(`{3,}|~{3,})([^\n]*)\s*$/);
|
||||||
|
if (!open) {
|
||||||
|
output.push(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
inFence = true;
|
||||||
|
fenceChar = open[1][0];
|
||||||
|
fenceLength = open[1].length;
|
||||||
|
fenceInfo = open[2] || "";
|
||||||
|
contentLines = [];
|
||||||
|
placeholder = `@@CODE_BLOCK_${blockIndex++}@@`;
|
||||||
|
output.push(placeholder);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeRe = new RegExp(`^ {0,3}${escapeRegExp(fenceChar)}{${fenceLength},}\\s*$`);
|
||||||
|
if (closeRe.test(line)) {
|
||||||
|
blocks.push({
|
||||||
|
placeholder,
|
||||||
|
html: buildCodeBlockHtml(fenceInfo, contentLines.join("\n")),
|
||||||
|
});
|
||||||
|
inFence = false;
|
||||||
|
fenceChar = "";
|
||||||
|
fenceLength = 0;
|
||||||
|
fenceInfo = "";
|
||||||
|
placeholder = "";
|
||||||
|
contentLines = [];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
contentLines.push(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inFence && placeholder) {
|
||||||
|
blocks.push({
|
||||||
|
placeholder,
|
||||||
|
html: buildCodeBlockHtml(fenceInfo, contentLines.join("\n")),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { masked: output.join("\n"), blocks };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseWithExplicitFences(markdown: string, breaks: boolean): string {
|
||||||
|
const { masked, blocks } = scanAndMaskFencedCodeBlocks(markdown);
|
||||||
|
let html = parseWithMarked(masked, breaks);
|
||||||
|
|
||||||
|
for (const block of blocks) {
|
||||||
|
html = html.replace(`<p>${block.placeholder}</p>`, block.html);
|
||||||
|
html = html.split(block.placeholder).join(block.html);
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cleanupAccidentalStandaloneInlineCode(html: string): string {
|
||||||
|
const standaloneMatches = html.match(/<(p|h[1-6]|li)><code>[\s\S]*?<\/code><\/\1>/g) || [];
|
||||||
|
if (standaloneMatches.length < 3) return html;
|
||||||
|
|
||||||
|
const headingCodeMatches = html.match(/<h[1-6]><code>[\s\S]*?<\/code><\/h[1-6]>/g) || [];
|
||||||
|
const suspiciousCount = standaloneMatches.filter((block) => /https?:\/\/|@\w|\/\s*\d{3,}|[一-龥]/.test(block)).length;
|
||||||
|
const hasPreBlock = /<pre>\s*<code(?:\s|>)/.test(html);
|
||||||
|
|
||||||
|
const looksMalformed =
|
||||||
|
headingCodeMatches.length > 0 ||
|
||||||
|
(hasPreBlock && standaloneMatches.length >= 5 && suspiciousCount >= 3);
|
||||||
|
if (!looksMalformed) return html;
|
||||||
|
|
||||||
|
return html.replace(/<(p|h[1-6]|li)><code>([\s\S]*?)<\/code><\/\1>/g, "<$1>$2</$1>");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markdownToImportHtml(markdown: string, options: MarkdownToHtmlOptions = {}): string {
|
||||||
|
const breaks = options.breaks ?? true;
|
||||||
|
const html = parseWithExplicitFences(markdown, breaks);
|
||||||
|
return cleanupAccidentalStandaloneInlineCode(html);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user