mirror of https://github.com/go-gitea/gitea
Support quote selected comments to reply (#32431)
Many existing tests were quite hacky, these could be improved later. <details> ![image](https://github.com/user-attachments/assets/93aebb4f-9de5-4cb8-910b-50c64cbcd25a) </details>pull/27151/head^2
parent
276500c314
commit
145e266987
@ -0,0 +1,24 @@ |
||||
import {convertHtmlToMarkdown} from './html2markdown.ts'; |
||||
import {createElementFromHTML} from '../utils/dom.ts'; |
||||
|
||||
const h = createElementFromHTML; |
||||
|
||||
test('convertHtmlToMarkdown', () => { |
||||
expect(convertHtmlToMarkdown(h(`<h1>h</h1>`))).toBe('# h'); |
||||
expect(convertHtmlToMarkdown(h(`<strong>txt</strong>`))).toBe('**txt**'); |
||||
expect(convertHtmlToMarkdown(h(`<em>txt</em>`))).toBe('_txt_'); |
||||
expect(convertHtmlToMarkdown(h(`<del>txt</del>`))).toBe('~~txt~~'); |
||||
|
||||
expect(convertHtmlToMarkdown(h(`<a href="link">txt</a>`))).toBe('[txt](link)'); |
||||
expect(convertHtmlToMarkdown(h(`<a href="https://link">https://link</a>`))).toBe('https://link'); |
||||
|
||||
expect(convertHtmlToMarkdown(h(`<img src="link">`))).toBe('![image](link)'); |
||||
expect(convertHtmlToMarkdown(h(`<img src="link" alt="name">`))).toBe('![name](link)'); |
||||
expect(convertHtmlToMarkdown(h(`<img src="link" width="1" height="1">`))).toBe('<img alt="image" width="1" height="1" src="link">'); |
||||
|
||||
expect(convertHtmlToMarkdown(h(`<p>txt</p>`))).toBe('txt\n'); |
||||
expect(convertHtmlToMarkdown(h(`<blockquote>a\nb</blockquote>`))).toBe('> a\n> b\n'); |
||||
|
||||
expect(convertHtmlToMarkdown(h(`<ol><li>a<ul><li>b</li></ul></li></ol>`))).toBe('1. a\n * b\n\n'); |
||||
expect(convertHtmlToMarkdown(h(`<ol><li><input checked>a</li></ol>`))).toBe('1. [x] a\n'); |
||||
}); |
@ -0,0 +1,119 @@ |
||||
import {htmlEscape} from 'escape-goat'; |
||||
|
||||
type Processors = { |
||||
[tagName: string]: (el: HTMLElement) => string | HTMLElement | void; |
||||
} |
||||
|
||||
type ProcessorContext = { |
||||
elementIsFirst: boolean; |
||||
elementIsLast: boolean; |
||||
listNestingLevel: number; |
||||
} |
||||
|
||||
function prepareProcessors(ctx:ProcessorContext): Processors { |
||||
const processors = { |
||||
H1(el) { |
||||
const level = parseInt(el.tagName.slice(1)); |
||||
el.textContent = `${'#'.repeat(level)} ${el.textContent.trim()}`; |
||||
}, |
||||
STRONG(el) { |
||||
return `**${el.textContent}**`; |
||||
}, |
||||
EM(el) { |
||||
return `_${el.textContent}_`; |
||||
}, |
||||
DEL(el) { |
||||
return `~~${el.textContent}~~`; |
||||
}, |
||||
|
||||
A(el) { |
||||
const text = el.textContent || 'link'; |
||||
const href = el.getAttribute('href'); |
||||
if (/^https?:/.test(text) && text === href) { |
||||
return text; |
||||
} |
||||
return href ? `[${text}](${href})` : text; |
||||
}, |
||||
IMG(el) { |
||||
const alt = el.getAttribute('alt') || 'image'; |
||||
const src = el.getAttribute('src'); |
||||
const widthAttr = el.hasAttribute('width') ? ` width="${htmlEscape(el.getAttribute('width') || '')}"` : ''; |
||||
const heightAttr = el.hasAttribute('height') ? ` height="${htmlEscape(el.getAttribute('height') || '')}"` : ''; |
||||
if (widthAttr || heightAttr) { |
||||
return `<img alt="${htmlEscape(alt)}"${widthAttr}${heightAttr} src="${htmlEscape(src)}">`; |
||||
} |
||||
return `![${alt}](${src})`; |
||||
}, |
||||
|
||||
P(el) { |
||||
el.textContent = `${el.textContent}\n`; |
||||
}, |
||||
BLOCKQUOTE(el) { |
||||
el.textContent = `${el.textContent.replace(/^/mg, '> ')}\n`; |
||||
}, |
||||
|
||||
OL(el) { |
||||
const preNewLine = ctx.listNestingLevel ? '\n' : ''; |
||||
el.textContent = `${preNewLine}${el.textContent}\n`; |
||||
}, |
||||
LI(el) { |
||||
const parent = el.parentNode; |
||||
const bullet = parent.tagName === 'OL' ? `1. ` : '* '; |
||||
const nestingIdentLevel = Math.max(0, ctx.listNestingLevel - 1); |
||||
el.textContent = `${' '.repeat(nestingIdentLevel * 4)}${bullet}${el.textContent}${ctx.elementIsLast ? '' : '\n'}`; |
||||
return el; |
||||
}, |
||||
INPUT(el) { |
||||
return el.checked ? '[x] ' : '[ ] '; |
||||
}, |
||||
|
||||
CODE(el) { |
||||
const text = el.textContent; |
||||
if (el.parentNode && el.parentNode.tagName === 'PRE') { |
||||
el.textContent = `\`\`\`\n${text}\n\`\`\`\n`; |
||||
return el; |
||||
} |
||||
if (text.includes('`')) { |
||||
return `\`\` ${text} \`\``; |
||||
} |
||||
return `\`${text}\``; |
||||
}, |
||||
}; |
||||
processors['UL'] = processors.OL; |
||||
for (let level = 2; level <= 6; level++) { |
||||
processors[`H${level}`] = processors.H1; |
||||
} |
||||
return processors; |
||||
} |
||||
|
||||
function processElement(ctx :ProcessorContext, processors: Processors, el: HTMLElement) { |
||||
if (el.hasAttribute('data-markdown-generated-content')) return el.textContent; |
||||
if (el.tagName === 'A' && el.children.length === 1 && el.children[0].tagName === 'IMG') { |
||||
return processElement(ctx, processors, el.children[0] as HTMLElement); |
||||
} |
||||
|
||||
const isListContainer = el.tagName === 'OL' || el.tagName === 'UL'; |
||||
if (isListContainer) ctx.listNestingLevel++; |
||||
for (let i = 0; i < el.children.length; i++) { |
||||
ctx.elementIsFirst = i === 0; |
||||
ctx.elementIsLast = i === el.children.length - 1; |
||||
processElement(ctx, processors, el.children[i] as HTMLElement); |
||||
} |
||||
if (isListContainer) ctx.listNestingLevel--; |
||||
|
||||
if (processors[el.tagName]) { |
||||
const ret = processors[el.tagName](el); |
||||
if (ret && ret !== el) { |
||||
el.replaceWith(typeof ret === 'string' ? document.createTextNode(ret) : ret); |
||||
} |
||||
} |
||||
} |
||||
|
||||
export function convertHtmlToMarkdown(el: HTMLElement): string { |
||||
const div = document.createElement('div'); |
||||
div.append(el); |
||||
const ctx = {} as ProcessorContext; |
||||
ctx.listNestingLevel = 0; |
||||
processElement(ctx, prepareProcessors(ctx), el); |
||||
return div.textContent; |
||||
} |
Loading…
Reference in new issue