Complete Forgejo discussion MVP
This commit is contained in:
parent
d84a885fdb
commit
51706d2d11
17 changed files with 1708 additions and 127 deletions
|
|
@ -20,7 +20,7 @@ function escapeHtml(value: string): string {
|
|||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function normalizeLinkTarget(value: string): string | null {
|
||||
function normalizeLinkTarget(value: string, baseUrl?: string): string | null {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
|
|
@ -31,7 +31,7 @@ function normalizeLinkTarget(value: string): string | null {
|
|||
}
|
||||
|
||||
try {
|
||||
const url = new URL(trimmed);
|
||||
const url = new URL(trimmed, baseUrl || undefined);
|
||||
if (url.protocol === "http:" || url.protocol === "https:") {
|
||||
return escapeHtml(url.toString());
|
||||
}
|
||||
|
|
@ -42,7 +42,7 @@ function normalizeLinkTarget(value: string): string | null {
|
|||
return null;
|
||||
}
|
||||
|
||||
function renderInline(markdown: string): string {
|
||||
function renderInline(markdown: string, baseUrl?: string): string {
|
||||
const codeTokens: string[] = [];
|
||||
let rendered = escapeHtml(markdown);
|
||||
|
||||
|
|
@ -51,10 +51,21 @@ function renderInline(markdown: string): string {
|
|||
codeTokens.push(`<code>${code}</code>`);
|
||||
return token;
|
||||
});
|
||||
rendered = rendered.replace(
|
||||
/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g,
|
||||
(_match, label: string, href: string) => {
|
||||
const safeHref = normalizeLinkTarget(href, baseUrl);
|
||||
if (!safeHref) {
|
||||
return label;
|
||||
}
|
||||
|
||||
return `<img src="${safeHref}" alt="${label}" loading="lazy" />`;
|
||||
},
|
||||
);
|
||||
rendered = rendered.replace(
|
||||
/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g,
|
||||
(_match, label: string, href: string) => {
|
||||
const safeHref = normalizeLinkTarget(href);
|
||||
const safeHref = normalizeLinkTarget(href, baseUrl);
|
||||
if (!safeHref) {
|
||||
return label;
|
||||
}
|
||||
|
|
@ -83,12 +94,12 @@ function createParserState(): ParserState {
|
|||
};
|
||||
}
|
||||
|
||||
function flushParagraph(state: ParserState) {
|
||||
function flushParagraph(state: ParserState, baseUrl?: string) {
|
||||
if (state.paragraphLines.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.output.push(`<p>${renderInline(state.paragraphLines.join(" "))}</p>`);
|
||||
state.output.push(`<p>${renderInline(state.paragraphLines.join(" "), baseUrl)}</p>`);
|
||||
state.paragraphLines.length = 0;
|
||||
}
|
||||
|
||||
|
|
@ -104,13 +115,13 @@ function flushList(state: ParserState) {
|
|||
state.listType = null;
|
||||
}
|
||||
|
||||
function flushBlockquote(state: ParserState) {
|
||||
function flushBlockquote(state: ParserState, baseUrl?: string) {
|
||||
if (state.blockquoteLines.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.output.push(
|
||||
`<blockquote><p>${renderInline(state.blockquoteLines.join(" "))}</p></blockquote>`,
|
||||
`<blockquote><p>${renderInline(state.blockquoteLines.join(" "), baseUrl)}</p></blockquote>`,
|
||||
);
|
||||
state.blockquoteLines.length = 0;
|
||||
}
|
||||
|
|
@ -131,10 +142,10 @@ function flushCodeBlock(state: ParserState) {
|
|||
state.codeLines.length = 0;
|
||||
}
|
||||
|
||||
function flushInlineBlocks(state: ParserState) {
|
||||
flushParagraph(state);
|
||||
function flushInlineBlocks(state: ParserState, baseUrl?: string) {
|
||||
flushParagraph(state, baseUrl);
|
||||
flushList(state);
|
||||
flushBlockquote(state);
|
||||
flushBlockquote(state, baseUrl);
|
||||
}
|
||||
|
||||
function handleCodeBlockLine(state: ParserState, line: string): boolean {
|
||||
|
|
@ -151,124 +162,129 @@ function handleCodeBlockLine(state: ParserState, line: string): boolean {
|
|||
return true;
|
||||
}
|
||||
|
||||
function handleFenceStart(state: ParserState, line: string): boolean {
|
||||
function handleFenceStart(state: ParserState, line: string, baseUrl?: string): boolean {
|
||||
if (!line.trim().startsWith("```")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
flushInlineBlocks(state);
|
||||
flushInlineBlocks(state, baseUrl);
|
||||
state.inCodeBlock = true;
|
||||
state.codeLanguage = line.trim().slice(3).trim();
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleBlankLine(state: ParserState, line: string): boolean {
|
||||
function handleBlankLine(state: ParserState, line: string, baseUrl?: string): boolean {
|
||||
if (line.trim()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
flushInlineBlocks(state);
|
||||
flushInlineBlocks(state, baseUrl);
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleHeadingLine(state: ParserState, line: string): boolean {
|
||||
function handleHeadingLine(state: ParserState, line: string, baseUrl?: string): boolean {
|
||||
const headingMatch = line.match(/^(#{1,6})\s+(.*)$/);
|
||||
if (!headingMatch) {
|
||||
return false;
|
||||
}
|
||||
|
||||
flushInlineBlocks(state);
|
||||
flushInlineBlocks(state, baseUrl);
|
||||
const level = headingMatch[1].length;
|
||||
state.output.push(`<h${level}>${renderInline(headingMatch[2].trim())}</h${level}>`);
|
||||
state.output.push(`<h${level}>${renderInline(headingMatch[2].trim(), baseUrl)}</h${level}>`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleRuleLine(state: ParserState, line: string): boolean {
|
||||
function handleRuleLine(state: ParserState, line: string, baseUrl?: string): boolean {
|
||||
if (!/^(-{3,}|\*{3,})$/.test(line.trim())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
flushInlineBlocks(state);
|
||||
flushInlineBlocks(state, baseUrl);
|
||||
state.output.push("<hr />");
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleListLine(state: ParserState, line: string, listType: ListType): boolean {
|
||||
function handleListLine(
|
||||
state: ParserState,
|
||||
line: string,
|
||||
listType: ListType,
|
||||
baseUrl?: string,
|
||||
): boolean {
|
||||
const pattern = listType === "ul" ? /^[-*+]\s+(.*)$/ : /^\d+\.\s+(.*)$/;
|
||||
const match = line.match(pattern);
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
|
||||
flushParagraph(state);
|
||||
flushBlockquote(state);
|
||||
flushParagraph(state, baseUrl);
|
||||
flushBlockquote(state, baseUrl);
|
||||
if (state.listType !== listType) {
|
||||
flushList(state);
|
||||
state.listType = listType;
|
||||
}
|
||||
state.listItems.push(renderInline(match[1].trim()));
|
||||
state.listItems.push(renderInline(match[1].trim(), baseUrl));
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleBlockquoteLine(state: ParserState, line: string): boolean {
|
||||
function handleBlockquoteLine(state: ParserState, line: string, baseUrl?: string): boolean {
|
||||
const match = line.match(/^>\s?(.*)$/);
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
|
||||
flushParagraph(state);
|
||||
flushParagraph(state, baseUrl);
|
||||
flushList(state);
|
||||
state.blockquoteLines.push(match[1].trim());
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleParagraphLine(state: ParserState, line: string) {
|
||||
function handleParagraphLine(state: ParserState, line: string, baseUrl?: string) {
|
||||
flushList(state);
|
||||
flushBlockquote(state);
|
||||
flushBlockquote(state, baseUrl);
|
||||
state.paragraphLines.push(line.trim());
|
||||
}
|
||||
|
||||
function processMarkdownLine(state: ParserState, line: string) {
|
||||
function processMarkdownLine(state: ParserState, line: string, baseUrl?: string) {
|
||||
if (handleCodeBlockLine(state, line)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (handleFenceStart(state, line)) {
|
||||
if (handleFenceStart(state, line, baseUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (handleBlankLine(state, line)) {
|
||||
if (handleBlankLine(state, line, baseUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (handleHeadingLine(state, line)) {
|
||||
if (handleHeadingLine(state, line, baseUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (handleRuleLine(state, line)) {
|
||||
if (handleRuleLine(state, line, baseUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (handleListLine(state, line, "ul") || handleListLine(state, line, "ol")) {
|
||||
if (handleListLine(state, line, "ul", baseUrl) || handleListLine(state, line, "ol", baseUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (handleBlockquoteLine(state, line)) {
|
||||
if (handleBlockquoteLine(state, line, baseUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleParagraphLine(state, line);
|
||||
handleParagraphLine(state, line, baseUrl);
|
||||
}
|
||||
|
||||
function markdownToHtml(markdown: string): string {
|
||||
function markdownToHtml(markdown: string, baseUrl?: string): string {
|
||||
const state = createParserState();
|
||||
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
||||
|
||||
for (const line of lines) {
|
||||
processMarkdownLine(state, line);
|
||||
processMarkdownLine(state, line, baseUrl);
|
||||
}
|
||||
|
||||
flushInlineBlocks(state);
|
||||
flushInlineBlocks(state, baseUrl);
|
||||
flushCodeBlock(state);
|
||||
return state.output.join("");
|
||||
}
|
||||
|
|
@ -288,8 +304,8 @@ export function stripLeadingTitleHeading(markdown: string, title: string): strin
|
|||
return markdown;
|
||||
}
|
||||
|
||||
export function MarkdownContent(props: { markdown: string; className?: string }) {
|
||||
const html = markdownToHtml(props.markdown);
|
||||
export function MarkdownContent(props: { markdown: string; className?: string; baseUrl?: string }) {
|
||||
const html = markdownToHtml(props.markdown, props.baseUrl);
|
||||
const className = props.className ? `markdown-content ${props.className}` : "markdown-content";
|
||||
|
||||
return <div className={className} dangerouslySetInnerHTML={{ __html: html }} />;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue