UNPKG

@mieweb/wikigdrive

Version:

Google Drive to MarkDown synchronization

467 lines (422 loc) 15 kB
import {Style, TextProperty} from './LibreOffice.ts'; import {inchesToPixels, spaces} from './utils.ts'; import {type MarkdownNode, MarkdownTagNode, OutputMode, TagPayload} from './MarkdownNodes.ts'; export function debugChunkToText(chunk: MarkdownNode) { if (chunk.isTag === false) { return chunk.text; } return chunk.tag; } export function addComment(chunk: MarkdownTagNode, comment: string) { if (chunk.comment) { chunk.comment += ' ' + comment; } else { chunk.comment = comment; } } export function textStyleToString(textProperty: TextProperty) { if (!textProperty) { return ''; } let styleTxt = ''; if (textProperty.fontColor) { styleTxt += ` fill: ${textProperty.fontColor};`; } if (textProperty.fontSize) { // styleTxt += ` font-size: ${inchesToMm(textProperty.fontSize)}mm;`; } return styleTxt; } function styleToString(style: Style) { let styleTxt = ''; if (style?.graphicProperties) { const graphicProperties = style?.graphicProperties; // if (graphicProperties.stroke) { // styleTxt += ` stroke: ${graphicProperties.stroke};`; // } if (graphicProperties.strokeWidth) { styleTxt += ` stroke-width: ${graphicProperties.strokeWidth};`; } if (graphicProperties.strokeColor) { styleTxt += ` stroke: ${graphicProperties.strokeColor};`; } if (graphicProperties.strokeLinejoin) { styleTxt += ` stroke-line-join: ${graphicProperties.strokeLinejoin};`; } // if (graphicProperties.fill) { // styleTxt += ` fill: ${graphicProperties.fill};`; // } if (graphicProperties.fillColor) { styleTxt += ` fill: ${graphicProperties.fillColor};`; } } if (!styleTxt) { return 'fill: transparent;'; } return styleTxt; } function buildSvgStart(payload: TagPayload) { const width = payload.width; const height = payload.height; let retVal = `<svg style="${payload.styleTxt || ''}" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg">\n`; const styleTxt = styleToString(payload?.style); if (styleTxt) { retVal += `<style>* { ${styleTxt} }</style>\n`; } return retVal; } interface ToTextContext { mode: OutputMode; onlyNotTag?: boolean; inListItem?: boolean; addLiIndents?: boolean; isMacro?: boolean; parentLevel?: number; } function addLiNumbers(chunk: MarkdownTagNode, ctx: {addLiIndents?: boolean, parentLevel?: number}, innerText: string) { if (!ctx.addLiIndents) { return innerText; } if (!chunk.isTag) { return innerText; } if (chunk.tag !== 'LI') { return innerText; } let noPara = false; if (chunk.children.length > 0 && chunk.children[0].isTag && chunk.children[0].tag === 'UL') { // No para, no symbol noPara = true; } const level = !ctx.parentLevel ? (chunk.payload.listLevel || 1) - 1 : 0; const indent = spaces(level * 4); const listStr = ((payload) => { if (payload.bullet) { return '* '; } if (payload.number && payload.number > 0) { if (['a'].includes(payload.numFormat || '')) { return String.fromCharCode('a'.charCodeAt(0) + payload.number - 1) + '. '; } if (['A'].includes(payload.numFormat || '')) { return String.fromCharCode('A'.charCodeAt(0) + payload.number - 1) + '. '; } if (['1', 'i', 'I'].includes(payload.numFormat || '')) { // TODO roman return `${chunk.payload.number}. `; } } return ''; })(chunk.payload); const firstStr = indent + listStr; const otherStr = indent + spaces(4); return innerText .split('\n') .map((line, idx, lines) => { if (idx === lines.length - 1 && line === '') { // Last line should be EOL, don't put spaces after it return line; } if (noPara) { return otherStr + '' + line; } if (idx === 0) { return firstStr + '' + line; } return otherStr + '' + line; }) .join('\n'); } function chunkToText(chunk: MarkdownNode, ctx: ToTextContext) { ctx = Object.assign({ mode: 'md' }, ctx); if (chunk.isTag === false) { return chunk.text; } if (ctx.inListItem) { switch (chunk.tag) { case 'B': return '<strong>' + chunksToText(chunk.children, ctx) + '</strong>'; case 'I': return '<em>' + chunksToText(chunk.children, ctx) + '</em>'; case 'BI': return '<strong><em>' + chunksToText(chunk.children, ctx) + '</em></strong>\''; } } switch (ctx.mode) { case 'raw': switch (chunk.tag) { case 'BODY': return chunksToText(chunk.children, ctx); case 'P': return chunksToText(chunk.children, ctx); case 'PRE': return chunksToText(chunk.children, ctx); case 'BR/': return '\n'; case 'EOL/': return '\n'; case 'EMPTY_LINE/': return '\n'; case 'BLANK/': return ''; } return chunksToText(chunk.children, ctx); case 'md': switch (chunk.tag) { case 'BODY': return chunksToText(chunk.children, ctx); case 'P': return chunksToText(chunk.children, ctx); case 'BR/': if (ctx.isMacro) { return '\n'; } return ' \n'; case 'EOL/': return '\n'; case 'EMPTY_LINE/': return '\n'; case 'PRE': return '```'+ (chunk.payload?.lang || '') +'\n' + chunksToText(chunk.children, ctx) + '```\n'; case 'CODE': return '`' + chunksToText(chunk.children, ctx) + '`'; case 'I': return '*' + chunksToText(chunk.children, ctx) + '*'; case 'BI': return '**_' + chunksToText(chunk.children, ctx) + '_**'; case 'B': return '**' + chunksToText(chunk.children, ctx) + '**'; case 'H1': return '# ' + chunksToText(chunk.children, ctx) + '\n'; case 'H2': // if (chunk.children.length === 0) { // TODO // return '\n'; // } return '## ' + chunksToText(chunk.children, ctx) + '\n'; case 'H3': return '### ' + chunksToText(chunk.children, ctx) + '\n'; case 'H4': return '#### ' + chunksToText(chunk.children, ctx) + '\n'; case 'HR/': return '___'; case 'A': return '[' + chunksToText(chunk.children, ctx) + `](${chunk.payload.href})`; case 'SVG/': return `![](${chunk.payload.href})`; case 'IMG/': return `![](${chunk.payload.href})`; case 'EMB_SVG': return buildSvgStart(chunk.payload); case 'HTML_MODE/': // TODO return chunksToText(chunk.children, { ...ctx, mode: 'html' }); case 'RAW_MODE/': return chunksToText(chunk.children, { ...ctx, mode: 'raw' }); case 'MACRO_MODE/': return chunksToText(chunk.children, { ...ctx, mode: 'md', isMacro: true }); case 'LI': // TODO return addLiNumbers(chunk, ctx, chunksToText(chunk.children, { ...ctx, inListItem: true, parentLevel: chunk.payload.listLevel })); case 'TOC': return chunksToText(chunk.children, ctx); // TODO case 'BOOKMARK/': return `<a id="${chunk.payload.id}"></a>`; } return chunksToText(chunk.children, ctx); case 'html': switch (chunk.tag) { case 'BODY': return chunksToText(chunk.children, ctx); case 'BR/': return '<br />\n'; case 'EOL/': return '\n'; case 'EMPTY_LINE/': return '<br />'; case 'HR/': return '<hr />'; case 'B': return '<strong>' + chunksToText(chunk.children, ctx) + '</strong>'; case 'I': return '<em>' + chunksToText(chunk.children, ctx) + '</em>'; case 'BI': return '<strong><em>' + chunksToText(chunk.children, ctx) + '</em></strong>\''; case 'H1': return '<h1>' + chunksToText(chunk.children, ctx) + '</h1>'; case 'H2': return '<h2>' + chunksToText(chunk.children, ctx) + '</h2>'; case 'H3': return '<h3>' + chunksToText(chunk.children, ctx) + '</h3>'; case 'H4': return '<h4>' + chunksToText(chunk.children, ctx) + '</h4>'; case 'P': return '<p>' + chunksToText(chunk.children, ctx) + '</p>'; case 'CODE': return '<code>' + chunksToText(chunk.children, ctx) + '</code>'; case 'PRE': return '<pre>' + chunksToText(chunk.children, ctx) + '</pre>'; case 'UL': if (chunk.payload.number > 0) { return '<ol>' + chunksToText(chunk.children, ctx) + '</ol>'; } else { return '<ul>' + chunksToText(chunk.children, ctx) + '</ul>'; } case 'LI': return '<li>' + chunksToText(chunk.children, ctx) + '</li>'; case 'A': return `<a href="${chunk.payload.href}">` + chunksToText(chunk.children, ctx) + '</a>'; case 'TABLE': return '<table>\n' + chunksToText(chunk.children, ctx) + '</table>\n'; case 'TR': return '<tr>\n' + chunksToText(chunk.children, ctx) + '</tr>\n'; case 'TD': return '<td>' + chunksToText(chunk.children, ctx) + '</td>\n'; case 'TOC': return chunksToText(chunk.children, ctx); case 'SVG/': return `<object type="image/svg+xml" data="${chunk.payload.href}" ></object>`; case 'IMG/': return `<img src="${chunk.payload.href}" />`; case 'EMB_SVG': return buildSvgStart(chunk.payload) + chunksToText(chunk.children, ctx) + '</svg>\n'; case 'EMB_SVG_G': { if (chunk.payload.x || chunk.payload.y) { const transformStr = `transform="translate(${chunk.payload.x || 0}, ${chunk.payload.y || 0})"`; return `<g ${transformStr}>\n` + chunksToText(chunk.children, ctx) + '</g>\n'; } return '<g>\n' + chunksToText(chunk.children, ctx) + '</g>\n'; } case 'EMB_SVG_P/': return `<path d="${chunk.payload.pathD}" transform="${chunk.payload.transform}" style="${styleToString(chunk.payload?.style)}" ></path>\n`; case 'EMB_SVG_TEXT': return `<text style="${chunk.payload.styleTxt || ''}" x="0" dy="100%" >` + chunksToText(chunk.children, ctx) + '</text>\n'; case 'EMB_SVG_TSPAN': { const fontSize = inchesToPixels(chunk.payload.style?.textProperties.fontSize); return `<tspan style="${textStyleToString(chunk.payload.style?.textProperties)}" font-size="${fontSize}">` + chunksToText(chunk.children, ctx) + '</tspan>\n'; } case 'BOOKMARK/': return `<a id="${chunk.payload.id}"></a>`; } return chunksToText(chunk.children, ctx); default: return ''; } } export function chunksToText(chunks: MarkdownNode[], ctx: ToTextContext): string { const retVal = []; ctx = Object.assign({ mode: 'md' }, ctx); for (let chunkNo = 0; chunkNo < chunks.length; chunkNo++) { const chunk = chunks[chunkNo]; retVal.push(chunkToText(chunk, ctx)); } return retVal.join(''); } export async function walkRecursiveAsync(node: MarkdownNode, callback: (node: MarkdownNode, ctx?: object) => Promise<object | void>, ctx?: object, callbackEnd?: (node: MarkdownNode, ctx?: object) => object | void) { if (node.isTag) { const subCtx = await callback(node, ctx) || Object.assign({}, ctx); for (let nodeIdx = 0; nodeIdx < node.children.length; nodeIdx++) { const child = node.children[nodeIdx]; const retVal = await walkRecursiveAsync(child, callback, { ...subCtx, nodeIdx }, callbackEnd); if (retVal && 'nodeIdx' in retVal && typeof retVal.nodeIdx === 'number') { nodeIdx = retVal.nodeIdx; } } if (callbackEnd) { callbackEnd(node, ctx); } return subCtx; } else { return await callback(node, ctx); } } export function walkRecursiveSync(node: MarkdownNode, callback: (node: MarkdownNode, ctx?: object) => object | void, ctx?: object, callbackEnd?: (node: MarkdownNode, ctx?: object) => object | void): void | object { if (node.isTag) { const subCtx = callback(node, ctx) || Object.assign({}, ctx); // let nodeIdx = 0; for (let nodeIdx = 0; nodeIdx < node.children.length; nodeIdx++) { const child = node.children[nodeIdx]; const retVal = walkRecursiveSync(child, callback, { ...subCtx, nodeIdx }, callbackEnd); if (retVal && 'nodeIdx' in retVal && typeof retVal.nodeIdx === 'number') { nodeIdx = retVal.nodeIdx; } } if (callbackEnd) { callbackEnd(node, ctx); } return subCtx; } else { return callback(node, ctx); } } export function extractText(node: MarkdownNode) { let retVal = ''; walkRecursiveSync(node, (child) => { if (child.isTag === false) { retVal += child.text; return; } if (child.isTag === true) { if (['BR/', 'EOL/', 'EMPTY_LINE/'].includes(child.tag)) { retVal += '\n'; } return; } }); return retVal; } export function dump(body: MarkdownTagNode, logger = console) { let position = 0; const stack = []; walkRecursiveSync(body, (chunk, ctx: { level: number }) => { let line = position + '\t'; if (chunk.isTag && ['HTML_MODE/', 'RAW_MODE/'].includes(chunk.tag)) { stack.push(chunk.tag); } const mode = stack[stack.length - 1] || 'MARK_DOWN/'; switch (mode) { case 'MARK_DOWN/': line += 'M '; break; case 'HTML_MODE/': line += 'H '; break; case 'RAW_MODE/': line += 'R '; break; } line += spaces(ctx.level); if (chunk.isTag === true) { line += chunk.tag; if (chunk.tag === 'PRE') { line += ` (Lang: ${chunk.payload.lang || ''})`; } if (chunk.tag === 'UL') { line += ` (Level: ${chunk.payload.listLevel}, #${chunk.payload?.number || ''})`; } if (chunk.tag === 'LI') { line += ` (${chunk.payload?.bullet || chunk.payload?.number}, Level: ${chunk.payload.listLevel})`; } } if (chunk.isTag === false) { line += chunk.text .replace(/\n/g, '\\n') .replace(/\t/g, '[TAB]'); } if (chunk.comment) { line += '\t// ' + chunk.comment; } if (logger === console) { // if (line.indexOf('StateMachine.ts:') > -1) { // console.log(ansi_colors.gray(line)); // continue; // } console.log(line); // continue; } else { logger.log(line); } position++; return { ...ctx, level: ctx.level + 1 }; }, { level: 0 }, (chunk) => { if (chunk.isTag && ['HTML_MODE/', 'RAW_MODE/'].includes(chunk.tag)) { stack.pop(); } }); }