feat(parser): add list and math support

This commit is contained in:
syy11cn 2022-05-30 00:33:06 +08:00
parent 4042f292cc
commit de303bf064
4 changed files with 187 additions and 13 deletions

View File

@ -1,7 +1,7 @@
import type Token from 'markdown-it/lib/token'
import MarkdownIt from 'markdown-it'
import type { ISectionOptions, IStylesOptions, Table, TableOfContents } from 'docx'
import { BorderStyle, Document, Footer, Header, Packer, PageNumber, Paragraph, SectionType, TextRun, convertInchesToTwip } from 'docx'
import { AlignmentType, BorderStyle, Document, Footer, Header, LevelFormat, Packer, PageNumber, Paragraph, SectionType, TextRun, convertInchesToTwip } from 'docx'
import type { IMarkdownReportConfig } from '@md-report/types'
import { StyleId } from '@md-report/types'
import { sliceParagraph, sliceSection } from './utils'
@ -35,6 +35,119 @@ export function parseDocument(tokens: Token[], styles: IStylesOptions): Document
pos += offset
}
return new Document({
numbering: {
config: [
{
reference: StyleId.ul,
levels: [
{
level: 0,
format: LevelFormat.BULLET,
text: '\u2022',
alignment: AlignmentType.LEFT,
style: {
paragraph: {
indent: { left: convertInchesToTwip(0.25), hanging: convertInchesToTwip(0.25) },
},
},
},
{
level: 1,
format: LevelFormat.BULLET,
text: '\u25E6',
alignment: AlignmentType.LEFT,
style: {
paragraph: {
indent: { left: convertInchesToTwip(0.5), hanging: convertInchesToTwip(0.25) },
},
},
},
{
level: 2,
format: LevelFormat.BULLET,
text: '\u25AA',
alignment: AlignmentType.LEFT,
style: {
paragraph: {
indent: { left: convertInchesToTwip(0.75), hanging: convertInchesToTwip(0.25) },
},
},
},
{
level: 3,
format: LevelFormat.BULLET,
text: '\u25AB',
alignment: AlignmentType.LEFT,
style: {
paragraph: {
indent: { left: convertInchesToTwip(1), hanging: convertInchesToTwip(0.25) },
},
},
},
{
level: 4,
format: LevelFormat.BULLET,
text: '\u2022',
alignment: AlignmentType.LEFT,
style: {
paragraph: {
indent: { left: convertInchesToTwip(1.25), hanging: convertInchesToTwip(0.25) },
},
},
},
],
},
{
reference: StyleId.ol,
levels: [
{
level: 0,
format: LevelFormat.DECIMAL,
text: '%1.',
alignment: AlignmentType.LEFT,
style: {
paragraph: {
indent: { left: convertInchesToTwip(0.25), hanging: convertInchesToTwip(0.25) },
},
},
},
{
level: 1,
format: LevelFormat.LOWER_LETTER,
text: '%2)',
alignment: AlignmentType.LEFT,
style: {
paragraph: {
indent: { left: convertInchesToTwip(0.5), hanging: convertInchesToTwip(0.25) },
},
},
},
{
level: 2,
format: LevelFormat.LOWER_ROMAN,
text: '%3.',
alignment: AlignmentType.LEFT,
style: {
paragraph: {
indent: { left: convertInchesToTwip(0.75), hanging: convertInchesToTwip(0.25) },
},
},
},
{
level: 3,
format: LevelFormat.DECIMAL,
text: '(%4)',
alignment: AlignmentType.LEFT,
style: {
paragraph: {
indent: { left: convertInchesToTwip(1), hanging: convertInchesToTwip(0.25) },
},
},
},
],
},
],
},
evenAndOddHeaderAndFooters: true,
styles,
sections,
@ -49,7 +162,11 @@ export function parseSection(tokens: Token[]): ISectionOptions {
while (pos < tokens.length) {
const { tokens: paragraph, offset } = sliceParagraph(tokens.slice(pos))
const parser = paragraphParser[paragraph[0].tag]
children.push(parser(paragraph))
const parseResult = parser(paragraph)
if (Array.isArray(parseResult))
children.push(...parseResult)
else
children.push(parseResult)
pos += offset
}
return {

View File

@ -7,25 +7,43 @@ import type Token from 'markdown-it/lib/token'
import { StyleId } from '@md-report/types'
import { sliceInlineText } from './utils'
export function parseInline(props: { tokens: Token[]; style?: StyleId; headingLevel?: number }): Paragraph {
// Variables.
const { tokens, style = StyleId.normal, headingLevel = 0 } = props
const { children: childrenTokens } = tokens[0]
export function parseInline(props: { tokens: Token[]; style?: StyleId; headingLevel?: number; isUL?: boolean; isOL?: boolean }): Paragraph {
// Props.
const { tokens, style = StyleId.normal, headingLevel = 0, isOL = false, isUL = false } = props
const { children: childrenTokens, level } = tokens[0]
if (!childrenTokens)
return new Paragraph({})
// Numbering.
let numbering: {
reference: string
level: number
} | undefined
if (isOL) {
numbering = {
reference: StyleId.ol,
level: (level - 1) / 2 - 1,
}
}
else if (isUL) {
numbering = {
reference: StyleId.ul,
level: (level - 1) / 2 - 1,
}
}
const children: ParagraphChild[] = []
let pos = 0
// Parse inline children.
while (pos < childrenTokens.length) {
const { tokens: paragraphChild, offset: nextPos } = sliceInlineText(childrenTokens.slice(pos))
const { tokens: paragraphChild, offset } = sliceInlineText(childrenTokens.slice(pos))
if (paragraphChild[0].tag === 'img')
children.push(parseImage(paragraphChild))
else
children.push(parseText(paragraphChild))
pos += nextPos
pos += offset
}
return new Paragraph({
style,
numbering,
heading: headingLevel ? HeadingLevel[`HEADING_${headingLevel}` as keyof typeof HeadingLevel] : undefined,
children,
})
@ -65,7 +83,15 @@ export function parseText(tokens: Token[]): TextRun {
// Inline code.
case 'code':
// TODO: Replace code font with env data.
options = { ...options, font: {}, text: token.content }
options = {
...options,
size: 20,
font: {
ascii: 'Monaco',
eastAsia: 'KaiTi',
},
text: token.content,
}
break
// Normal text.
default:

View File

@ -1,8 +1,8 @@
import type Token from 'markdown-it/lib/token'
import type { IBorderOptions } from 'docx'
import { BorderStyle, Paragraph, Table, TableCell, TableRow, TextRun } from 'docx'
import { BorderStyle, Math, MathRun, Paragraph, Table, TableCell, TableRow, TextRun } from 'docx'
import { StyleId } from '@md-report/types'
import { sliceTableRow } from './utils'
import { MathBlockRegExp, sliceTableRow } from './utils'
import { parseInline } from './inline'
export function parseFence(tokens: Token[]): Paragraph {
@ -59,7 +59,19 @@ export function parseParagraph(tokens: Token[]): Paragraph {
let style = StyleId.normal
if (inline[0].children?.length === 1 && inline[0].children[0].tag === 'img')
style = StyleId.image
// Math blocks.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, mathBlock] = inline[0].content.match(MathBlockRegExp) || []
// eslint-disable-next-line no-console
console.log(mathBlock)
if (mathBlock) {
return new Paragraph({
style: StyleId.image,
children: [new Math({
children: [new MathRun(mathBlock)],
})],
})
}
return parseInline({
tokens: inline,
style,
@ -78,10 +90,27 @@ export function parseHeading(tokens: Token[]): Paragraph {
})
}
export const paragraphParser: Record<string, (tokens: Token[]) => (Paragraph|Table)> = {
export function parseList(type: StyleId.ol | StyleId.ul): (tokens: Token[]) => Paragraph[] {
const parser = (tokens: Token[]) => {
let pos = 0
const children: Paragraph[] = []
while (pos < tokens.length) {
if (tokens[pos].type === 'inline')
children.push(parseInline({ tokens: [tokens[pos]], style: StyleId.list, isUL: type === StyleId.ul, isOL: type === StyleId.ol }))
pos++
}
return children
}
return parser
}
export const paragraphParser: Record<string, (tokens: Token[]) => (Paragraph | Table | Paragraph[])> = {
code: parseFence,
table: parseTable,
p: parseParagraph,
ul: parseList(StyleId.ul),
ol: parseList(StyleId.ol),
h1: parseHeading,
h2: parseHeading,
h3: parseHeading,

View File

@ -54,3 +54,5 @@ export function sliceInlineText(tokens: Token[]): SliceResult {
}
return sliceParagraph(tokens)
}
export const MathBlockRegExp = /^\$\$\n([^]*)\n\$\$$/