diff --git a/README.md b/README.md deleted file mode 120000 index d69bbc38..00000000 --- a/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# wikiGDrive - -[Start Here for documentation](website/docs/_index.md) - -Google Drive to MarkDown synchronization - -[![Develop Server Deploy](https://github.com/mieweb/wikiGDrive/actions/workflows/DevelopServerDeploy.yml/badge.svg?branch=develop&event=push)](https://github.com/mieweb/wikiGDrive/actions/workflows/DevelopServerDeploy.yml) -[![Prod Server Deploy](https://github.com/mieweb/wikiGDrive/actions/workflows/ProdServerDeploy.yml/badge.svg?branch=master&event=push)](https://github.com/mieweb/wikiGDrive/actions/workflows/ProdServerDeploy.yml) -[![CodeQL](https://github.com/mieweb/wikiGDrive/actions/workflows/codeql-analysis.yml/badge.svg?branch=master&event=push)](https://github.com/mieweb/wikiGDrive/actions/workflows/codeql-analysis.yml?query=event%3Apush+branch%3Amaster+) - -WikiGDrive is a node app that uses the [Google Drive API](https://developers.google.com/drive/api/v3/quickstart/nodejs) to transform Google Docs and Drawings into markdown. diff --git a/README.md b/README.md new file mode 100644 index 00000000..9335232d --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# wikiGDrive + +Google Drive to MarkDown synchronization + +[![Develop Server Deploy](https://github.com/mieweb/wikiGDrive/actions/workflows/DevelopServerDeploy.yml/badge.svg?branch=develop&event=push)](https://github.com/mieweb/wikiGDrive/actions/workflows/DevelopServerDeploy.yml) +[![Prod Server Deploy](https://github.com/mieweb/wikiGDrive/actions/workflows/ProdServerDeploy.yml/badge.svg?branch=master&event=push)](https://github.com/mieweb/wikiGDrive/actions/workflows/ProdServerDeploy.yml) +[![CodeQL](https://github.com/mieweb/wikiGDrive/actions/workflows/codeql-analysis.yml/badge.svg?branch=master&event=push)](https://github.com/mieweb/wikiGDrive/actions/workflows/codeql-analysis.yml?query=event%3Apush+branch%3Amaster+) + +WikiGDrive is a node app that uses the [Google Drive API](https://developers.google.com/drive/api/v3/quickstart/nodejs) to transform Google Docs and Drawings into markdown. + +With a "Shared Drive" as the key, WikiGDrive: + +* Reads all the files from a Google "Shared Drive" +* Builds a map of the driveId (URL) to the pathname in the "Shared Drive" +* For each Google Document: + * Converts to a Markdown file with the path (instead of the driveId for the file) + * Changes driveId to the path (eg: 12lvdxKgGsD.../edit would be changed to /filename + * Support diagrams as SVG (and map the URLs in the diagram) + +WikiGDrive scans for changes in the drive and then refresh the local converted files. + +## Usage + +* [Usage](./website/docs/usage/wikigdrive-usage.md) + +## Developer Documentation + +* [Developer README](./website/docs/developer-guide.md) +* [Internals](./website/docs/internals.md) diff --git a/apps/ui/src/components/PreviewHeader.vue b/apps/ui/src/components/PreviewHeader.vue index c899292c..da9f73cb 100644 --- a/apps/ui/src/components/PreviewHeader.vue +++ b/apps/ui/src/components/PreviewHeader.vue @@ -109,11 +109,11 @@ export default { } const folderPath = this.folderPath.endsWith('/') ? this.folderPath : this.folderPath + '/'; - const filePath = folderPath + this.selectedFile.fileName; + const filePaths = folderPath + this.selectedFile.fileName; await this.commit({ message: this.commitMsg, - filePath + filePaths }); this.commitMsg = ''; }, diff --git a/apps/ui/src/pages/GDocsView.vue b/apps/ui/src/pages/GDocsView.vue index fe92d934..fec91701 100644 --- a/apps/ui/src/pages/GDocsView.vue +++ b/apps/ui/src/pages/GDocsView.vue @@ -329,11 +329,11 @@ export default { } const folderPath = this.folderPath.endsWith('/') ? this.folderPath : this.folderPath + '/'; - const filePath = folderPath + this.selectedFile.fileName; + const filePaths = folderPath + this.selectedFile.fileName; await this.commit({ message: this.commitMsg, - filePath + filePaths }); this.commitMsg = ''; return true; diff --git a/src/cli/usage.ts b/src/cli/usage.ts index bad6a77b..1180ba72 100644 --- a/src/cli/usage.ts +++ b/src/cli/usage.ts @@ -29,6 +29,27 @@ function locateUsage(usageMarkdown: string, sectionPrefix: string): string { return retVal.join('\n'); } +function indentMarkdownCodes(markdown: string) { + const retVal = []; + + const lines = markdown.split('\n'); + let inCode = false; + for (const line of lines) { + if (line === '```') { + inCode = !inCode; + continue; + } + + if (inCode) { + retVal.push(' ' + line); + } else { + retVal.push(line); + } + } + + return retVal.join('\n'); +} + export async function usage(filename: string) { const pkg = JSON.parse(new TextDecoder().decode(fs.readFileSync(path.resolve(__dirname, '..', '..', 'package.json')))); @@ -38,13 +59,15 @@ export async function usage(filename: string) { const sectionName = filename.replace(/^.*-(.*).ts/, '$1'); - const mdFilename = execName + '_usage.md'; + const mdFilename = execName + '-usage.md'; const usageMarkdown = new TextDecoder().decode(fs.readFileSync(path.resolve(__dirname, '..', '..', 'website', 'docs', 'usage', mdFilename))); - const commandUsage = locateUsage(usageMarkdown, `${execName} ${sectionName}`) || locateUsage(usageMarkdown, `${execName} usage`); - const allCommands = locateUsage(usageMarkdown, 'All commands'); - const commonOptions = locateUsage(usageMarkdown, 'Common options'); + const indentedMarkdown = indentMarkdownCodes(usageMarkdown); + + const commandUsage = locateUsage(indentedMarkdown, `${execName} ${sectionName}`) || locateUsage(indentedMarkdown, `${execName} usage`); + const allCommands = locateUsage(indentedMarkdown, 'All commands'); + const commonOptions = locateUsage(indentedMarkdown, 'Common options'); console.log( `${pkg.name} version: ${pkg.version}, ${process.env.GIT_SHA}\n\nUsage:\n${commandUsage.trim()}\n\n${commonOptions.trim()}\n\n${allCommands.trim()}`); diff --git a/src/containers/transform/LocalLog.ts b/src/containers/transform/LocalLog.ts index 1d3d0063..cd9eea75 100644 --- a/src/containers/transform/LocalLog.ts +++ b/src/containers/transform/LocalLog.ts @@ -91,17 +91,4 @@ export class LocalLog { return originalLength !== this.rows.length; } - async getDirFiles(prefix: string): Promise { - const list = this.rows - .filter(row => row.filePath.startsWith(prefix) && row.filePath.substring(prefix.length).indexOf('/') === -1) - .filter(row => row.type === 'md'); - - const lastOnes: {[key: string]: LogRow} = {}; - for (const item of list) { - lastOnes[item.filePath] = item; - } - - return Object.values(lastOnes).filter(item => item.event !== 'removed'); - } - } diff --git a/src/containers/transform/TransformContainer.ts b/src/containers/transform/TransformContainer.ts index 485e8383..bc989d95 100644 --- a/src/containers/transform/TransformContainer.ts +++ b/src/containers/transform/TransformContainer.ts @@ -558,20 +558,4 @@ export class TransformContainer extends Container { onProgressNotify(callback: ({total, completed, warnings, failed}: { total?: number; completed?: number, warnings?: number, failed?: number }) => void) { this.progressNotifyCallback = callback; } - - async removeOutdatedLogEntries(destinationDirectory: FileContentService, destinationFiles: LocalFileMap) { - const prefix = destinationDirectory.getVirtualPath(); - const logFiles = await this.localLog.getDirFiles(prefix); - for (const logEntry of logFiles) { - const fileName = logEntry.filePath.substring(prefix.length); - if (!destinationFiles[fileName]) { - this.localLog.append({ - filePath: logEntry.filePath, - id: logEntry.id, - type: logEntry.type, - event: 'removed', - }); - } - } - } } diff --git a/src/odt/MarkdownChunks.ts b/src/odt/MarkdownChunks.ts index 882b2fca..20feaf94 100644 --- a/src/odt/MarkdownChunks.ts +++ b/src/odt/MarkdownChunks.ts @@ -12,7 +12,7 @@ export type TAG = 'HR/' | 'BR/' | 'B' | '/B' | 'I' | '/I' | 'BI' | '/BI' | 'TOC' | '/TOC' | 'SVG/' | 'IMG/' | 'EMB_SVG' | '/EMB_SVG' | 'EMB_SVG_G' | '/EMB_SVG_G' | 'EMB_SVG_P/' | 'EMB_SVG_TEXT' | '/EMB_SVG_TEXT' | 'EMB_SVG_TSPAN' | '/EMB_SVG_TSPAN' | - 'CHANGE' | '/CHANGE' | 'HTML_MODE/' | 'MD_MODE/'; + 'CHANGE' | '/CHANGE' | 'HTML_MODE/' | 'MD_MODE/' | 'COMMENT'; export const isOpening = (tag: TAG) => !tag.startsWith('/') && !tag.endsWith('/'); export const isClosing = (tag: TAG) => tag.startsWith('/'); diff --git a/src/odt/OdtToMarkdown.ts b/src/odt/OdtToMarkdown.ts index 58bae926..ff67a7e2 100644 --- a/src/odt/OdtToMarkdown.ts +++ b/src/odt/OdtToMarkdown.ts @@ -23,6 +23,7 @@ import {inchesToPixels, inchesToSpaces, spaces} from './utils.ts'; import {extractPath} from './extractPath.ts'; import {mergeDeep} from './mergeDeep.ts'; import {RewriteRule} from './applyRewriteRule.ts'; +import {postProcessText} from './postprocess/postProcessText.js'; function getBaseFileName(fileName) { return fileName.replace(/.*\//, ''); @@ -120,7 +121,8 @@ export class OdtToMarkdown { const markdown = this.chunks.toString(this.rewriteRules); const trimmed = this.trimBreaks(markdown); - return await this.rewriteHeaders(trimmed); + const rewrittenHeaders = await this.rewriteHeaders(trimmed); + return postProcessText(rewrittenHeaders); } trimBreaks(markdown: string) { diff --git a/src/odt/StateMachine.ts b/src/odt/StateMachine.ts index 4bb6e0d8..069e283d 100644 --- a/src/odt/StateMachine.ts +++ b/src/odt/StateMachine.ts @@ -1,8 +1,20 @@ import slugify from 'slugify'; import {isClosing, isOpening, MarkdownChunks, OutputMode, TAG, TagPayload} from './MarkdownChunks.ts'; -import {fixCharacters, inchesToSpaces, spaces} from './utils.ts'; +import {fixCharacters} from './utils.ts'; import {RewriteRule} from './applyRewriteRule.ts'; +import {postProcessHeaders} from './postprocess/postProcessHeaders.js'; +import {postProcessPreMacros} from './postprocess/postProcessPreMacros.js'; +import {addIndentsAndBullets} from './postprocess/addIndentsAndBullets.js'; +import {fixBold} from './postprocess/fixBold.js'; +import {hideSuggestedChanges} from './postprocess/hideSuggestedChanges.js'; +import {addEmptyLines} from './postprocess/addEmptyLines.js'; +import {mergeParagraphs} from './postprocess/mergeParagraphs.js'; +import {removePreWrappingAroundMacros} from './postprocess/removePreWrappingAroundMacros.js'; +import {fixListParagraphs} from './postprocess/fixListParagraphs.js'; +import {fixSpacesInsideInlineFormatting} from './postprocess/fixSpacesInsideInlineFormatting.js'; +import {removeInsideDoubleCodeBegin} from './postprocess/removeInsideDoubleCodeBegin.js'; +import {trimEndOfParagraphs} from './postprocess/trimEndOfParagraphs.js'; interface TagLeaf { mode: OutputMode; @@ -48,22 +60,6 @@ export function stripMarkdownMacro(innerTxt) { return innerTxt; } -function isPreBeginMacro(innerTxt: string) { - return innerTxt.startsWith('{{% pre ') && innerTxt.endsWith(' %}}'); -} - -function isPreEndMacro(innerTxt: string) { - return innerTxt.startsWith('{{% /pre ') && innerTxt.endsWith(' %}}'); -} - -function isBeginMacro(innerTxt: string) { - return innerTxt.startsWith('{{% ') && !innerTxt.startsWith('{{% /') && innerTxt.endsWith(' %}}'); -} - -function isEndMacro(innerTxt: string) { - return innerTxt.startsWith('{{% /') && innerTxt.endsWith(' %}}'); -} - export class StateMachine { public errors: string[] = []; private readonly tagsTree: TagLeaf[] = []; @@ -157,6 +153,22 @@ export class StateMachine { this.storeListNo(listStyleName, payload.number); } } +/* List indents should be determined from marginLefts, which are different per doc. Damnit !!! + if (tag === 'P') { + if (this.currentLevel?.tag === 'LI') { + // payload.listLevel = this.currentLevel.payload.listLevel; + const listStyleName = (payload.listStyle?.name || this.getParentListStyleName()) + '_' + payload.listLevel; + payload.listLevel = inchesToSpaces(payload.style?.paragraphProperties?.marginLeft) / 2; + + if (payload.listLevel !== this.currentLevel.payload.listLevel) { + console.log('EEEEEEEEEEEEEEE', payload.listLevel, this.currentLevel.payload.listLevel, payload.style?.paragraphProperties?.marginLeft); + } + payload.number = payload.number || this.fetchListNo(listStyleName); + payload.number++; + this.storeListNo(listStyleName, payload.number); + } + } +*/ // PRE-PUSH-PRE-TREEPUSH @@ -228,11 +240,14 @@ export class StateMachine { } } + // Inside list item tags like needs to be html tags if (this.currentMode === 'md' && tag === '/P' && this.parentLevel?.tag === 'LI') { for (let pos = this.currentLevel.payload.position + 1; pos < payload.position; pos++) { const chunk = this.markdownChunks.chunks[pos]; if (chunk.isTag && chunk.tag === 'A') continue; if (chunk.isTag && chunk.tag === '/A') continue; + if (chunk.isTag && chunk.tag === 'IMG/') continue; + if (chunk.isTag && chunk.tag === 'SVG/') continue; chunk.mode = 'html'; } @@ -306,10 +321,12 @@ export class StateMachine { { switch (innerTxt) { case '{{/rawhtml}}': + // this.markdownChunks[payload.position].comment = 'Switching to md - {{/rawhtml}}'; this.currentMode = 'md'; break; } if (isMarkdownEndMacro(innerTxt)) { + // this.markdownChunks[payload.position].comment = 'Switching to md - isMarkdownEndMacro'; this.currentMode = 'md'; } } @@ -325,10 +342,12 @@ export class StateMachine { switch (innerTxt) { case '{{rawhtml}}': + // this.markdownChunks[payload.position].comment = 'Switching to raw - {{rawhtml}}'; this.currentMode = 'raw'; break; } if (isMarkdownBeginMacro(innerTxt)) { + // this.markdownChunks[payload.position].comment = 'Switching to raw - isMarkdownBeginMacro'; this.currentMode = 'raw'; } @@ -381,384 +400,18 @@ export class StateMachine { } postProcess() { - for (let position = 0; position < this.markdownChunks.length; position++) { - const chunk = this.markdownChunks.chunks[position]; - - if (chunk.isTag && ['/H1', '/H2', '/H3', '/H4'].indexOf(chunk.tag) > -1) { - const prevChunk = this.markdownChunks.chunks[position - 1]; - const tagOpening = chunk.tag.substring(1); - if (prevChunk.isTag && prevChunk.tag === tagOpening) { - this.markdownChunks.removeChunk(position); - this.markdownChunks.removeChunk(position - 1); - position -= 2; - continue; - } - } - - - if (chunk.isTag && chunk.tag === 'PRE') { - const prevChunk = this.markdownChunks.chunks[position - 1]; - if (prevChunk.isTag && prevChunk.tag === 'P') { - this.markdownChunks.removeChunk(position - 1); - position--; - continue; - } - } - - if (chunk.isTag && chunk.tag === '/PRE') { - const prevChunk = this.markdownChunks.chunks[position + 1]; - if (prevChunk?.isTag && prevChunk.tag === '/P') { - this.markdownChunks.removeChunk(position + 1); - position--; - continue; - } - } - } - - for (let position = 0; position < this.markdownChunks.length; position++) { - const chunk = this.markdownChunks.chunks[position]; - if (chunk.isTag === false && isMarkdownBeginMacro(chunk.text)) { - const prevChunk = this.markdownChunks.chunks[position - 1]; - const postChunk = this.markdownChunks.chunks[position + 1]; - if (prevChunk.isTag && prevChunk.tag === 'PRE' && postChunk.isTag && postChunk.tag === '/PRE') { - this.markdownChunks.removeChunk(position - 1); - postChunk.tag = 'PRE'; - position--; - continue; - } - } - - if (chunk.isTag === false && isMarkdownEndMacro(chunk.text)) { - const preChunk = this.markdownChunks.chunks[position - 1]; - const postChunk = this.markdownChunks.chunks[position + 1]; - if (preChunk.isTag && preChunk.tag === 'PRE' && postChunk.isTag && postChunk.tag === '/PRE') { - preChunk.tag = '/PRE'; - this.markdownChunks.removeChunk(position + 1); - position--; - continue; - } - } - } - - for (let position = 0; position < this.markdownChunks.length; position++) { - const chunk = this.markdownChunks.chunks[position]; - if (chunk.isTag === false && chunk.text.startsWith('```') && chunk.text.length > 3) { - const preChunk = this.markdownChunks.chunks[position - 2]; - if (preChunk.isTag && preChunk.tag === 'PRE') { - preChunk.payload.lang = chunk.text.substring(3); - this.markdownChunks.removeChunk(position); - position--; - continue; - } - } - } - - for (let position = 1; position < this.markdownChunks.length; position++) { - const chunk = this.markdownChunks.chunks[position]; - if (chunk.isTag && ['/B', '/I'].indexOf(chunk.tag) > -1) { - const prevChunk = this.markdownChunks.chunks[position - 1]; - if (prevChunk.isTag === false && prevChunk.mode === 'md') { - const text = prevChunk.text; - const removedTrailingSpaces = text.replace(/[\s]+$/, ''); - const spaces = text.substring(removedTrailingSpaces.length); - if (spaces.length > 0) { - prevChunk.text = removedTrailingSpaces; - this.markdownChunks.chunks.splice(position + 1, 0, { - isTag: false, - mode: 'md', - text: spaces - }); - position++; - } - } - } - } - - const matching = { - '/B': 'B', - '/I': 'I' - }; - - const double = ['B', 'I', '/B', '/I']; - - for (let position = 1; position < this.markdownChunks.length; position++) { - const chunk = this.markdownChunks.chunks[position]; - if (chunk.isTag && Object.keys(matching).indexOf(chunk.tag) > -1) { - const prevChunk = this.markdownChunks.chunks[position - 1]; - if (prevChunk.isTag && prevChunk.tag === matching[chunk.tag]) { - this.markdownChunks.removeChunk(position); - this.markdownChunks.removeChunk(position - 1); - position-=2; - continue; - } - } - - if (chunk.isTag && ['PRE'].indexOf(chunk.tag) > -1) { - const prevChunk = this.markdownChunks.chunks[position - 1]; - if (prevChunk.isTag && prevChunk.tag === '/PRE') { - prevChunk.tag = 'BR/'; - this.markdownChunks.removeChunk(position); - position--; - continue; - } - } - - if (chunk.isTag && double.indexOf(chunk.tag) > -1) { - const prevChunk = this.markdownChunks.chunks[position - 1]; - if (prevChunk.isTag && prevChunk.tag == chunk.tag) { - this.markdownChunks.removeChunk(position); - position--; - continue; - } - } - } - - let nextPara = null; - for (let position = this.markdownChunks.length - 1; position >= 0; position--) { - const chunk = this.markdownChunks.chunks[position]; - if (chunk.isTag && chunk.tag === 'P') { - if (nextPara) { - if (nextPara.payload?.listLevel && !chunk.payload?.listLevel) { - chunk.payload.listLevel = nextPara?.payload?.listLevel; - } - if (!chunk.payload?.bullet && nextPara.payload?.number === chunk.payload?.number && nextPara.payload?.listLevel === chunk.payload?.listLevel) { - delete nextPara.payload.number; - } - } - nextPara = chunk; - } - } - - let inChange = false; - for (let position = 0; position < this.markdownChunks.length; position++) { - const chunk = this.markdownChunks.chunks[position]; - if (chunk.isTag && chunk.tag === 'CHANGE') { - inChange = true; - this.markdownChunks.removeChunk(position); - position--; - continue; - } - if (chunk.isTag && chunk.tag === '/CHANGE') { - inChange = false; - this.markdownChunks.removeChunk(position); - position--; - continue; - } - - if (inChange) { - this.markdownChunks.removeChunk(position); - position--; - } - } - - for (let position = 0; position < this.markdownChunks.length; position++) { - const chunk = this.markdownChunks.chunks[position]; - - if (position + 1 < this.markdownChunks.chunks.length && chunk.isTag && ['/H1', '/H2', '/H3', '/H4', 'IMG/', 'SVG/'].indexOf(chunk.tag) > -1) { - const nextTag = this.markdownChunks.chunks[position + 1]; - - if (!(nextTag.isTag && nextTag.tag === 'BR/')) { - this.markdownChunks.chunks.splice(position + 1, 0, { - isTag: true, - mode: 'md', - tag: 'BR/', - payload: {}, - comment: 'Next tag is not BR/' - }); - } - } - - if (position > 1 && chunk.isTag && ['H1', 'H2', 'H3', 'H4', 'IMG/', 'SVG/'].indexOf(chunk.tag) > -1) { - const prevTag = this.markdownChunks.chunks[position - 1]; - if (!(prevTag.isTag && prevTag.tag === 'BR/')) { - this.markdownChunks.chunks.splice(position - 1, 0, { - isTag: false, - mode: 'md', - text: '\n', - // payload: {}, - comment: 'Add empty line before: ' + chunk.tag - }); - position++; - } - } - } - - // ADD indents and bullets - for (let position = 0; position < this.markdownChunks.length; position++) { - const chunk = this.markdownChunks.chunks[position]; - if (chunk.isTag === true && chunk.tag === 'P' && chunk.mode === 'md') { - const level = (chunk.payload.listLevel || 1) - 1; - let indent = spaces(level * 3); - if (chunk.payload.style?.paragraphProperties?.marginLeft) { - indent = spaces(inchesToSpaces(chunk.payload.style?.paragraphProperties?.marginLeft) - 4); - } - const listStr = chunk.payload.bullet ? '* ' : chunk.payload.number > 0 ? `${chunk.payload.number}. ` : ''; - const firstStr = indent + listStr; - const otherStr = indent + spaces(listStr.length); - - let prevEmptyLine = 1; - for (let position2 = position + 1; position2 < this.markdownChunks.length; position2++) { - const chunk2 = this.markdownChunks.chunks[position2]; - if (chunk2.isTag === true && chunk2.tag === '/P' && chunk.mode === 'md') { - position += position2 - position - 1; - break; - } - - if (chunk2.isTag === true && ['BR/'].indexOf(chunk2.tag) > -1) { - prevEmptyLine = 2; - continue; - } - - if (chunk2.isTag === false && chunk2.text.startsWith('{{% ') && chunk2.text.endsWith(' %}}')) { - const innerText = chunk2.text.substring(3, chunk2.text.length - 3); - if (innerText.indexOf(' %}}') === -1) { - continue; - } - } - - if (prevEmptyLine > 0) { - this.markdownChunks.chunks.splice(position2, 0, { - mode: 'md', - isTag: false, - text: prevEmptyLine === 1 ? firstStr : otherStr - }); - prevEmptyLine = 0; - position2++; - } - } - } - } - - for (let position = 1; position < this.markdownChunks.length; position++) { - const chunk = this.markdownChunks.chunks[position]; - - if (chunk.isTag === false && chunk.mode === 'md') { - const prevChunk = this.markdownChunks.chunks[position - 1]; - if (prevChunk.isTag === false && prevChunk.mode === 'md') { - prevChunk.text = prevChunk.text + chunk.text; - this.markdownChunks.removeChunk(position); - position-=2; - continue; - } - } - - if (chunk.isTag === false && isPreBeginMacro(chunk.text)) { - const prevChunk = this.markdownChunks.chunks[position - 1]; - if (prevChunk.isTag && prevChunk.tag === 'PRE') { - this.markdownChunks.chunks.splice(position + 1, 0, { - isTag: true, - tag: 'PRE', - mode: 'md', - payload: {} - }); - this.markdownChunks.removeChunk(position - 1); - position--; - continue; - } - } - - if (chunk.isTag === false && isPreEndMacro(chunk.text)) { - const postChunk = this.markdownChunks.chunks[position + 1]; - if (postChunk.isTag && postChunk.tag === '/PRE') { - this.markdownChunks.removeChunk(position + 1); - this.markdownChunks.chunks.splice(position, 0, { - isTag: true, - tag: '/PRE', - mode: 'md', - payload: {} - }); - } - } - } - - let previousParaPosition = 0; - const macros = []; - for (let position = 0; position < this.markdownChunks.length - 1; position++) { - const chunk = this.markdownChunks.chunks[position]; - - if (chunk.isTag && chunk.mode === 'md' && chunk.tag === 'P') { - previousParaPosition = position; - continue; - } - - if (chunk.isTag === false && chunk.mode === 'md' && isBeginMacro(chunk.text)) { - macros.push(chunk.text); - continue; - } - - if (chunk.isTag === false && chunk.mode === 'md' && isEndMacro(chunk.text)) { - continue; - } - - if (chunk.isTag && chunk.mode === 'md' && chunk.tag === '/P') { - const nextChunk = this.markdownChunks.chunks[position + 1]; - - if (macros.length > 0) { - continue; - } - - if (nextChunk.isTag && nextChunk.mode === 'md' && nextChunk.tag === 'P') { - - let nextParaClosing = 0; - for (let position2 = position + 1; position2 < this.markdownChunks.length; position2++) { - const chunk2 = this.markdownChunks.chunks[position2]; - if (chunk2.isTag && chunk2.mode === 'md' && chunk2.tag === '/P') { - nextParaClosing = position2; - break; - } - } - - if (nextParaClosing > 0) { - const innerText = this.markdownChunks.extractText(position, nextParaClosing, this.rewriteRules); - if (innerText.length === 0) { - continue; - } - } - - if (previousParaPosition > 0) { - const innerText = this.markdownChunks.extractText(previousParaPosition, position, this.rewriteRules); - if (innerText.length === 0) { - continue; - } - if (innerText.endsWith(' %}}')) { - continue; - } - } - - const findFirstTextAfterPos = (start: number): string | null => { - for (let pos = start + 1; pos < this.markdownChunks.chunks.length; pos++) { - if ('text' in this.markdownChunks.chunks[pos]) { - return this.markdownChunks.chunks[pos].text; - } - } - return null; - }; - - const nextText = findFirstTextAfterPos(nextParaClosing); - if (nextText === '* ' || nextText?.trim().length === 0) { - this.markdownChunks.chunks.splice(position, 2, { - isTag: false, - text: '\n', - mode: 'md', - comment: 'End of line, but next line is list' - }); - position--; - previousParaPosition = 0; - } else { - this.markdownChunks.chunks.splice(position, 2, { - isTag: true, - tag: 'BR/', - mode: 'md', - payload: {}, - comment: 'End of line, two paras merge together' - }); - position--; - previousParaPosition = 0; - } - - } - } - } + postProcessHeaders(this.markdownChunks); + removePreWrappingAroundMacros(this.markdownChunks); + removeInsideDoubleCodeBegin(this.markdownChunks); + fixSpacesInsideInlineFormatting(this.markdownChunks); + fixBold(this.markdownChunks); + fixListParagraphs(this.markdownChunks); + hideSuggestedChanges(this.markdownChunks); + addEmptyLines(this.markdownChunks); + addIndentsAndBullets(this.markdownChunks); + postProcessPreMacros(this.markdownChunks); + mergeParagraphs(this.markdownChunks, this.rewriteRules); + trimEndOfParagraphs(this.markdownChunks); if (process.env.DEBUG_COLORS) { this.markdownChunks.dump(); diff --git a/src/odt/postprocess/addEmptyLines.ts b/src/odt/postprocess/addEmptyLines.ts new file mode 100644 index 00000000..7e3a55c3 --- /dev/null +++ b/src/odt/postprocess/addEmptyLines.ts @@ -0,0 +1,37 @@ +import {MarkdownChunks} from '../MarkdownChunks.js'; + +export function addEmptyLines(markdownChunks: MarkdownChunks) { + + for (let position = 0; position < markdownChunks.length; position++) { + const chunk = markdownChunks.chunks[position]; + + if (position + 1 < markdownChunks.chunks.length && chunk.isTag && ['/H1', '/H2', '/H3', '/H4', 'SVG/', '/UL'].indexOf(chunk.tag) > -1) { + const nextTag = markdownChunks.chunks[position + 1]; + + if (!(nextTag.isTag && nextTag.tag === 'BR/') && !(nextTag.isTag && nextTag.tag === '/TD')) { + markdownChunks.chunks.splice(position + 1, 0, { + isTag: true, + mode: 'md', + tag: 'BR/', + payload: {}, + comment: 'Next tag is not BR/' + }); + } + } + + if (position > 1 && chunk.isTag && ['H1', 'H2', 'H3', 'H4', 'SVG/', 'UL'].indexOf(chunk.tag) > -1) { + const prevTag = markdownChunks.chunks[position - 1]; + if (!(prevTag.isTag && prevTag.tag === 'BR/') && !(prevTag.isTag && prevTag.tag === 'TD')) { + markdownChunks.chunks.splice(position, 0, { + isTag: false, + mode: 'md', + text: '\n', + // payload: {}, + comment: 'Add empty line before: ' + chunk.tag + }); + position++; + } + } + } + +} diff --git a/src/odt/postprocess/addIndentsAndBullets.ts b/src/odt/postprocess/addIndentsAndBullets.ts new file mode 100644 index 00000000..5cb7ed1c --- /dev/null +++ b/src/odt/postprocess/addIndentsAndBullets.ts @@ -0,0 +1,53 @@ +import {MarkdownChunks} from '../MarkdownChunks.js'; +import {spaces} from '../utils.js'; + +export function addIndentsAndBullets(markdownChunks: MarkdownChunks) { +// ADD indents and bullets + for (let position = 0; position < markdownChunks.length; position++) { + const chunk = markdownChunks.chunks[position]; + if (chunk.isTag === true && chunk.tag === 'P' && chunk.mode === 'md') { + const level = (chunk.payload.listLevel || 1) - 1; + // let indent = spaces(level * 4); GDocs not fully compatible + // if (chunk.payload.style?.paragraphProperties?.marginLeft) { + // indent = spaces(inchesToSpaces(chunk.payload.style?.paragraphProperties?.marginLeft) - 4); + // } + const indent = spaces(level * 3); + const listStr = chunk.payload.bullet ? '* ' : chunk.payload.number > 0 ? `${chunk.payload.number}. ` : ''; + const firstStr = indent + listStr; + const otherStr = indent + spaces(listStr.length); + + let prevEmptyLine = 1; + for (let position2 = position + 1; position2 < markdownChunks.length; position2++) { + const chunk2 = markdownChunks.chunks[position2]; + if (chunk2.isTag === true && chunk2.tag === '/P' && chunk.mode === 'md') { + position += position2 - position - 1; + break; + } + + if (chunk2.isTag === true && ['BR/'].indexOf(chunk2.tag) > -1) { + prevEmptyLine = 2; + continue; + } + + if (chunk2.isTag === false && chunk2.text.startsWith('{{% ') && chunk2.text.endsWith(' %}}')) { + const innerText = chunk2.text.substring(3, chunk2.text.length - 3); + if (innerText.indexOf(' %}}') === -1) { + continue; + } + } + + if (prevEmptyLine > 0) { + markdownChunks.chunks.splice(position2, 0, { + mode: 'md', + isTag: false, + text: prevEmptyLine === 1 ? firstStr : otherStr, + comment: 'Indent or bullet, level: ' + level + }); + prevEmptyLine = 0; + position2++; + } + } + } + } + +} diff --git a/src/odt/postprocess/fixBold.ts b/src/odt/postprocess/fixBold.ts new file mode 100644 index 00000000..8625aac4 --- /dev/null +++ b/src/odt/postprocess/fixBold.ts @@ -0,0 +1,44 @@ +import {MarkdownChunks} from '../MarkdownChunks.js'; + +export function fixBold(markdownChunks: MarkdownChunks) { + + const matching = { + '/B': 'B', + '/I': 'I' + }; + + const double = ['B', 'I', '/B', '/I']; + + for (let position = 1; position < markdownChunks.length; position++) { + const chunk = markdownChunks.chunks[position]; + if (chunk.isTag && Object.keys(matching).indexOf(chunk.tag) > -1) { + const prevChunk = markdownChunks.chunks[position - 1]; + if (prevChunk.isTag && prevChunk.tag === matching[chunk.tag]) { + markdownChunks.removeChunk(position); + markdownChunks.removeChunk(position - 1); + position-=2; + continue; + } + } + + if (chunk.isTag && ['PRE'].indexOf(chunk.tag) > -1) { + const prevChunk = markdownChunks.chunks[position - 1]; + if (prevChunk.isTag && prevChunk.tag === '/PRE') { + prevChunk.tag = 'BR/'; + markdownChunks.removeChunk(position); + position--; + continue; + } + } + + if (chunk.isTag && double.indexOf(chunk.tag) > -1) { + const prevChunk = markdownChunks.chunks[position - 1]; + if (prevChunk.isTag && prevChunk.tag == chunk.tag) { + markdownChunks.removeChunk(position); + position--; + continue; + } + } + } + +} diff --git a/src/odt/postprocess/fixListParagraphs.ts b/src/odt/postprocess/fixListParagraphs.ts new file mode 100644 index 00000000..1b2e5aac --- /dev/null +++ b/src/odt/postprocess/fixListParagraphs.ts @@ -0,0 +1,19 @@ +import {MarkdownChunks} from '../MarkdownChunks.js'; + +export function fixListParagraphs(markdownChunks: MarkdownChunks) { + let nextPara = null; + for (let position = markdownChunks.length - 1; position >= 0; position--) { + const chunk = markdownChunks.chunks[position]; + if (chunk.isTag && chunk.tag === 'P') { + if (nextPara) { + if (nextPara.payload?.listLevel && !chunk.payload?.listLevel) { + chunk.payload.listLevel = nextPara?.payload?.listLevel; + } + if (!chunk.payload?.bullet && nextPara.payload?.number === chunk.payload?.number && nextPara.payload?.listLevel === chunk.payload?.listLevel) { + delete nextPara.payload.number; + } + } + nextPara = chunk; + } + } +} diff --git a/src/odt/postprocess/fixSpacesInsideInlineFormatting.ts b/src/odt/postprocess/fixSpacesInsideInlineFormatting.ts new file mode 100644 index 00000000..6dcd64c5 --- /dev/null +++ b/src/odt/postprocess/fixSpacesInsideInlineFormatting.ts @@ -0,0 +1,24 @@ +import {MarkdownChunks} from '../MarkdownChunks.js'; + +export function fixSpacesInsideInlineFormatting(markdownChunks: MarkdownChunks) { + for (let position = 1; position < markdownChunks.length; position++) { + const chunk = markdownChunks.chunks[position]; + if (chunk.isTag && ['/B', '/I'].indexOf(chunk.tag) > -1) { + const prevChunk = markdownChunks.chunks[position - 1]; + if (prevChunk.isTag === false && prevChunk.mode === 'md') { + const text = prevChunk.text; + const removedTrailingSpaces = text.replace(/[\s]+$/, ''); + const spaces = text.substring(removedTrailingSpaces.length); + if (spaces.length > 0) { + prevChunk.text = removedTrailingSpaces; + markdownChunks.chunks.splice(position + 1, 0, { + isTag: false, + mode: 'md', + text: spaces + }); + position++; + } + } + } + } +} diff --git a/src/odt/postprocess/hideSuggestedChanges.ts b/src/odt/postprocess/hideSuggestedChanges.ts new file mode 100644 index 00000000..8960a13f --- /dev/null +++ b/src/odt/postprocess/hideSuggestedChanges.ts @@ -0,0 +1,26 @@ +import {MarkdownChunks} from '../MarkdownChunks.js'; + +export function hideSuggestedChanges(markdownChunks: MarkdownChunks) { + let inChange = false; + for (let position = 0; position < markdownChunks.length; position++) { + const chunk = markdownChunks.chunks[position]; + if (chunk.isTag && chunk.tag === 'CHANGE') { + inChange = true; + markdownChunks.removeChunk(position); + position--; + continue; + } + if (chunk.isTag && chunk.tag === '/CHANGE') { + inChange = false; + markdownChunks.removeChunk(position); + position--; + continue; + } + + if (inChange) { + markdownChunks.removeChunk(position); + position--; + } + } + +} diff --git a/src/odt/postprocess/mergeParagraphs.ts b/src/odt/postprocess/mergeParagraphs.ts new file mode 100644 index 00000000..001a4e7f --- /dev/null +++ b/src/odt/postprocess/mergeParagraphs.ts @@ -0,0 +1,102 @@ +import {MarkdownChunks} from '../MarkdownChunks.js'; +import {RewriteRule} from '../applyRewriteRule.js'; + +export function isBeginMacro(innerTxt: string) { + return innerTxt.startsWith('{{% ') && !innerTxt.startsWith('{{% /') && innerTxt.endsWith(' %}}'); +} + +export function isEndMacro(innerTxt: string) { + return innerTxt.startsWith('{{% /') && innerTxt.endsWith(' %}}'); +} + +export function mergeParagraphs(markdownChunks: MarkdownChunks, rewriteRules: RewriteRule[]) { + let previousParaPosition = 0; + const macros = []; + for (let position = 0; position < markdownChunks.length - 1; position++) { + const chunk = markdownChunks.chunks[position]; + + if (chunk.isTag && chunk.mode === 'md' && chunk.tag === 'P') { + previousParaPosition = position; + continue; + } + + if (chunk.isTag === false && chunk.mode === 'md' && isBeginMacro(chunk.text)) { + macros.push(chunk.text); + continue; + } + + if (chunk.isTag === false && chunk.mode === 'md' && isEndMacro(chunk.text)) { + continue; + } + + if (chunk.isTag && chunk.mode === 'md' && chunk.tag === '/P') { + const nextChunk = markdownChunks.chunks[position + 1]; + + if (macros.length > 0) { + continue; + } + + if (nextChunk.isTag && nextChunk.mode === 'md' && nextChunk.tag === 'P') { + + let nextParaClosing = 0; + for (let position2 = position + 1; position2 < markdownChunks.length; position2++) { + const chunk2 = markdownChunks.chunks[position2]; + if (chunk2.isTag && chunk2.mode === 'md' && chunk2.tag === '/P') { + nextParaClosing = position2; + break; + } + } + + if (nextParaClosing > 0) { + const innerText = markdownChunks.extractText(position, nextParaClosing, rewriteRules); + if (innerText.length === 0) { + continue; + } + } + + if (previousParaPosition > 0) { + const innerText = markdownChunks.extractText(previousParaPosition, position, rewriteRules); + if (innerText.length === 0) { + continue; + } + if (innerText.endsWith(' %}}')) { + continue; + } + } + + const findFirstTextAfterPos = (start: number): string | null => { + for (let pos = start + 1; pos < markdownChunks.chunks.length; pos++) { + const currentChunk = markdownChunks.chunks[pos]; + if ('text' in currentChunk) { + return currentChunk.text; + } + } + return null; + }; + + const nextText = findFirstTextAfterPos(nextParaClosing); + if (nextText === '* ' || nextText?.trim().length === 0) { + markdownChunks.chunks.splice(position, 2, { + isTag: false, + text: '\n', + mode: 'md', + comment: 'End of line, but next line is list' + }); + position--; + previousParaPosition = 0; + } else { + markdownChunks.chunks.splice(position, 2, { + isTag: true, + tag: 'BR/', + mode: 'md', + payload: {}, + comment: 'End of line, two paras merge together' + }); + position--; + previousParaPosition = 0; + } + + } + } + } +} diff --git a/src/odt/postprocess/postProcessHeaders.ts b/src/odt/postprocess/postProcessHeaders.ts new file mode 100644 index 00000000..fffc4b59 --- /dev/null +++ b/src/odt/postprocess/postProcessHeaders.ts @@ -0,0 +1,37 @@ +import {MarkdownChunks} from '../MarkdownChunks.js'; + +export function postProcessHeaders(markdownChunks: MarkdownChunks) { + for (let position = 0; position < markdownChunks.length; position++) { + const chunk = markdownChunks.chunks[position]; + + if (chunk.isTag && ['/H1', '/H2', '/H3', '/H4'].indexOf(chunk.tag) > -1) { + const prevChunk = markdownChunks.chunks[position - 1]; + const tagOpening = chunk.tag.substring(1); + if (prevChunk.isTag && prevChunk.tag === tagOpening) { + markdownChunks.removeChunk(position); + markdownChunks.removeChunk(position - 1); + position -= 2; + continue; + } + } + + + if (chunk.isTag && chunk.tag === 'PRE') { + const prevChunk = markdownChunks.chunks[position - 1]; + if (prevChunk.isTag && prevChunk.tag === 'P') { + markdownChunks.removeChunk(position - 1); + position--; + continue; + } + } + + if (chunk.isTag && chunk.tag === '/PRE') { + const prevChunk = markdownChunks.chunks[position + 1]; + if (prevChunk?.isTag && prevChunk.tag === '/P') { + markdownChunks.removeChunk(position + 1); + position--; + continue; + } + } + } +} diff --git a/src/odt/postprocess/postProcessPreMacros.ts b/src/odt/postprocess/postProcessPreMacros.ts new file mode 100644 index 00000000..04f5dc32 --- /dev/null +++ b/src/odt/postprocess/postProcessPreMacros.ts @@ -0,0 +1,53 @@ +import {MarkdownChunks} from '../MarkdownChunks.js'; + +function isPreBeginMacro(innerTxt: string) { + return innerTxt.startsWith('{{% pre ') && innerTxt.endsWith(' %}}'); +} + +function isPreEndMacro(innerTxt: string) { + return innerTxt.startsWith('{{% /pre ') && innerTxt.endsWith(' %}}'); +} + +export function postProcessPreMacros(markdownChunks: MarkdownChunks) { + for (let position = 1; position < markdownChunks.length; position++) { + const chunk = markdownChunks.chunks[position]; + + if (chunk.isTag === false && chunk.mode === 'md') { + const prevChunk = markdownChunks.chunks[position - 1]; + if (prevChunk.isTag === false && prevChunk.mode === 'md') { + prevChunk.text = prevChunk.text + chunk.text; + markdownChunks.removeChunk(position); + position-=2; + continue; + } + } + + if (chunk.isTag === false && isPreBeginMacro(chunk.text)) { + const prevChunk = markdownChunks.chunks[position - 1]; + if (prevChunk.isTag && prevChunk.tag === 'PRE') { + markdownChunks.chunks.splice(position + 1, 0, { + isTag: true, + tag: 'PRE', + mode: 'md', + payload: {} + }); + markdownChunks.removeChunk(position - 1); + position--; + continue; + } + } + + if (chunk.isTag === false && isPreEndMacro(chunk.text)) { + const postChunk = markdownChunks.chunks[position + 1]; + if (postChunk.isTag && postChunk.tag === '/PRE') { + markdownChunks.removeChunk(position + 1); + markdownChunks.chunks.splice(position, 0, { + isTag: true, + tag: '/PRE', + mode: 'md', + payload: {} + }); + } + } + } +} diff --git a/src/odt/postprocess/postProcessText.ts b/src/odt/postprocess/postProcessText.ts new file mode 100644 index 00000000..9aa5930c --- /dev/null +++ b/src/odt/postprocess/postProcessText.ts @@ -0,0 +1,70 @@ +import {} from '../StateMachine.js'; +import {isBeginMacro, isEndMacro} from './mergeParagraphs.js'; + +export function emptyString(str: string) { + return str.trim().length === 0; +} + +export function postProcessText(markdown: string): string { + const lines = markdown.split('\n'); + + function addEmptyLineBefore(lineNo: number) { + lines.splice(lineNo, 0, ''); + if (lineNo > 0 && lines[lineNo - 1].endsWith(' ')) { + lines[lineNo - 1] = lines[lineNo - 1].replace(/ +$/, ''); + } + } + + function hasClosingMacroAfter(lineNo: number, currentLine: string) { + const closingText = currentLine.replace('{{% ', '{{% /'); + for (let idx = lineNo + 1; idx < lines.length; idx++) { + if (lines[idx] === closingText) { + return true; + } + } + return false; + } + + for (let lineNo = 1; lineNo < lines.length; lineNo++) { + const prevLine = lines[lineNo - 1]; + const currentLine = lines[lineNo]; + if (isBeginMacro(currentLine) && !emptyString(prevLine) && hasClosingMacroAfter(lineNo, currentLine)) { + addEmptyLineBefore(lineNo); + lineNo--; + continue; + } + } + + for (let lineNo = 0; lineNo < lines.length - 1; lineNo++) { + const currentLine = lines[lineNo]; + const nextLine = lines[lineNo + 1]; + if (isBeginMacro(currentLine) && emptyString(nextLine) && hasClosingMacroAfter(lineNo, currentLine)) { + lines.splice(lineNo + 1, 1); + lineNo--; + continue; + } + } + + for (let lineNo = 0; lineNo < lines.length - 1; lineNo++) { + const currentLine = lines[lineNo]; + const nextLine = lines[lineNo + 1]; + if (isEndMacro(currentLine) && !emptyString(nextLine)) { + addEmptyLineBefore(lineNo + 1); + lineNo--; + continue; + } + } + + for (let lineNo = 1; lineNo < lines.length; lineNo++) { + const prevLine = lines[lineNo - 1]; + const currentLine = lines[lineNo]; + if (isEndMacro(currentLine) && emptyString(prevLine)) { + lines.splice(lineNo - 1, 1); + lineNo--; + continue; + } + } + + + return lines.join('\n'); +} diff --git a/src/odt/postprocess/removeInsideDoubleCodeBegin.ts b/src/odt/postprocess/removeInsideDoubleCodeBegin.ts new file mode 100644 index 00000000..db7563eb --- /dev/null +++ b/src/odt/postprocess/removeInsideDoubleCodeBegin.ts @@ -0,0 +1,16 @@ +import {MarkdownChunks} from '../MarkdownChunks.js'; + +export function removeInsideDoubleCodeBegin(markdownChunks: MarkdownChunks) { + for (let position = 0; position < markdownChunks.length; position++) { + const chunk = markdownChunks.chunks[position]; + if (chunk.isTag === false && chunk.text.startsWith('```') && chunk.text.length > 3) { + const preChunk = markdownChunks.chunks[position - 2]; + if (preChunk.isTag && preChunk.tag === 'PRE') { + preChunk.payload.lang = chunk.text.substring(3); + markdownChunks.removeChunk(position); + position--; + continue; + } + } + } +} diff --git a/src/odt/postprocess/removePreWrappingAroundMacros.ts b/src/odt/postprocess/removePreWrappingAroundMacros.ts new file mode 100644 index 00000000..231fa890 --- /dev/null +++ b/src/odt/postprocess/removePreWrappingAroundMacros.ts @@ -0,0 +1,29 @@ +import {MarkdownChunks} from '../MarkdownChunks.js'; +import {isMarkdownBeginMacro, isMarkdownEndMacro} from '../StateMachine.js'; + +export function removePreWrappingAroundMacros(markdownChunks: MarkdownChunks) { + for (let position = 0; position < markdownChunks.length; position++) { + const chunk = markdownChunks.chunks[position]; + if (chunk.isTag === false && isMarkdownBeginMacro(chunk.text)) { + const prevChunk = markdownChunks.chunks[position - 1]; + const postChunk = markdownChunks.chunks[position + 1]; + if (prevChunk.isTag && prevChunk.tag === 'PRE' && postChunk.isTag && postChunk.tag === '/PRE') { + markdownChunks.removeChunk(position - 1); + postChunk.tag = 'PRE'; + position--; + continue; + } + } + + if (chunk.isTag === false && isMarkdownEndMacro(chunk.text)) { + const preChunk = markdownChunks.chunks[position - 1]; + const postChunk = markdownChunks.chunks[position + 1]; + if (preChunk.isTag && preChunk.tag === 'PRE' && postChunk.isTag && postChunk.tag === '/PRE') { + preChunk.tag = '/PRE'; + markdownChunks.removeChunk(position + 1); + position--; + continue; + } + } + } +} diff --git a/src/odt/postprocess/trimEndOfParagraphs.ts b/src/odt/postprocess/trimEndOfParagraphs.ts new file mode 100644 index 00000000..a19931bf --- /dev/null +++ b/src/odt/postprocess/trimEndOfParagraphs.ts @@ -0,0 +1,14 @@ +import {MarkdownChunks} from '../MarkdownChunks.js'; + +export function trimEndOfParagraphs(markdownChunks: MarkdownChunks) { + for (let position = 1; position < markdownChunks.length; position++) { + const chunk = markdownChunks.chunks[position]; + + if (chunk.isTag === true && chunk.tag === '/P') { + const prevChunk = markdownChunks.chunks[position - 1]; + if (prevChunk.isTag === false) { + prevChunk.text = prevChunk.text.replace(/ +$/, ''); + } + } + } +} diff --git a/test/odt_md/Issues.test.ts b/test/odt_md/Issues.test.ts new file mode 100644 index 00000000..089fd6f3 --- /dev/null +++ b/test/odt_md/Issues.test.ts @@ -0,0 +1,74 @@ +import {assert} from 'chai'; +import fs from 'fs'; + +import {compareTexts} from '../utils.ts'; +import {OdtToMarkdown} from '../../src/odt/OdtToMarkdown.ts'; +import {DocumentContent, DocumentStyles, LIBREOFFICE_CLASSES} from '../../src/odt/LibreOffice.ts'; +import {UnMarshaller} from '../../src/odt/UnMarshaller.ts'; +import {OdtProcessor} from '../../src/odt/OdtProcessor.ts'; +import {FileContentService} from '../../src/utils/FileContentService.ts'; + +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +describe('MarkDownTransformTest', () => { + + it('test ./issue-431', async () => { + // https://github.com/mieweb/wikiGDrive/issues/431 + const testMarkdown = fs.readFileSync(__dirname + '/issue-431.md').toString(); + const markdown = await transformOdt('issue-431'); + assert.ok(compareTexts(testMarkdown, markdown, false)); + }); + + it('test ./issue-432', async () => { + // https://github.com/mieweb/wikiGDrive/issues/432 + const testMarkdown = fs.readFileSync(__dirname + '/issue-432.md').toString(); + const markdown = await transformOdt('issue-432'); + assert.ok(compareTexts(testMarkdown, markdown, false)); + }); + + it('test ./issue-434', async () => { + return; // Should we convert this fake list into real? + // https://github.com/mieweb/wikiGDrive/issues/434 + const testMarkdown = fs.readFileSync(__dirname + '/issue-434.md').toString(); + const markdown = await transformOdt('issue-434'); + assert.ok(compareTexts(testMarkdown, markdown, false)); + }); + + it('test ./issue-435-436', async () => { + // https://github.com/mieweb/wikiGDrive/issues/435 + // https://github.com/mieweb/wikiGDrive/issues/436 + const testMarkdown = fs.readFileSync(__dirname + '/issue-435-436.md').toString(); + const markdown = await transformOdt('issue-435-436'); + assert.ok(compareTexts(testMarkdown, markdown, false)); + }); + +}); +async function transformOdt(id: string) { + const folder = new FileContentService(__dirname); + const odtPath = folder.getRealPath() + '/' + id + '.odt'; + const processor = new OdtProcessor(odtPath); + await processor.load(); + if (!processor.getContentXml()) { + throw Error('No odt processed'); + } + return transform(processor.getContentXml(), processor.getStylesXml()); +} + +async function transform(contentXml: string, stylesXml: string) { + const parser = new UnMarshaller(LIBREOFFICE_CLASSES, 'DocumentContent'); + const document: DocumentContent = parser.unmarshal(contentXml); + if (!document) { + throw Error('No document unmarshalled'); + } + const parserStyles = new UnMarshaller(LIBREOFFICE_CLASSES, 'DocumentStyles'); + const styles: DocumentStyles = parserStyles.unmarshal(stylesXml); + if (!styles) { + throw Error('No styles unmarshalled'); + } + const converter = new OdtToMarkdown(document, styles); + return await converter.convert(); +} diff --git a/test/odt_md/confluence.md b/test/odt_md/confluence.md index ef0fdf7f..dfec21fb 100644 --- a/test/odt_md/confluence.md +++ b/test/odt_md/confluence.md @@ -15,6 +15,7 @@ A new github repo with a node.js script specific to this conversion. * Scan all of the documents in a Confluence Space * Make google documents in a shared drive (two passes will be required so links between documents can be known as content is added). + * Parent/Child relationship must be intact. * Import Confluence "Attachments" to Google Drive so they can be referenced. diff --git a/test/odt_md/example-document.md b/test/odt_md/example-document.md index af55a3ef..61dac7af 100644 --- a/test/odt_md/example-document.md +++ b/test/odt_md/example-document.md @@ -166,12 +166,12 @@ This is after the horizontal line. * Bullet 1 * Bullet 2 - * SubBullet 1 - * SubBullet 2 + * SubBullet 1 + * SubBullet 2 * Bullet 3 - 1. SubNumeric 1 - 2. SubNumeric 2 - 3. SubNumeric 3 +1. SubNumeric 1 +2. SubNumeric 2 +3. SubNumeric 3 1. Alpha 1 2. Alpha 2 3. Alpha 3 diff --git a/test/odt_md/extract_xml.sh b/test/odt_md/extract_xml.sh new file mode 100755 index 00000000..b75a7ada --- /dev/null +++ b/test/odt_md/extract_xml.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +#unzip -j $1 content.xml -d $1.xml +unzip -p $1 content.xml | xmllint --format - > $1.xml + + + diff --git a/test/odt_md/issue-431.md b/test/odt_md/issue-431.md new file mode 100644 index 00000000..3cf841fc --- /dev/null +++ b/test/odt_md/issue-431.md @@ -0,0 +1,10 @@ +## Editing Scanned Batches + +Once documents have been scanned into the WebScan module, users may make any necessary edits to the batch, before uploading the scanned documents into the respective charts. + +{{% info %}} +Indexing can be done immediately following scanning, or saved for a later time. +{{% /info %}} + +**Move Left** or **Move Right**: Users can use the Move Left or Move Right button to rearrange the pages of a batch. After using the Prev and Next buttons to navigate through the pages, users can then use the Move Left button to place the current page being displayed ahead (i.e., to the left) of the next page. Clicking the Move Right button will place the current page after (i.e., to the right) one page. +**Crop**: Users may Crop a document prior to indexing it to a chart. First, navigate to the page needing cropped, using the Prev or Next button, accordingly. To utilize the Crop feature, simply hover the mouse over the scanned image and place the cross cursor (+) at the starting point of the intended crop. Left-click and hold the mouse button, dragging the cursor over the scanned image, highlighting (in black) the area intended to be cropped and kept. Release the mouse. If the highlighted field needs redone, simply left-click the mouse again, and resize the crop field. When ready, click the **Crop** button. The area highlighted in black will be saved. After clicking the Crop button, the image will refresh, showing the cropped document in the upper-left corner. The cropped image will be stored in the chart, once uploaded. If more of the document requires cropping, simply continue by repeating the steps, above. diff --git a/test/odt_md/issue-431.odt b/test/odt_md/issue-431.odt new file mode 100644 index 00000000..73515c9c Binary files /dev/null and b/test/odt_md/issue-431.odt differ diff --git a/test/odt_md/issue-432.md b/test/odt_md/issue-432.md new file mode 100644 index 00000000..a1117e81 --- /dev/null +++ b/test/odt_md/issue-432.md @@ -0,0 +1,19 @@ +#### Test Methodology + +MIE will report a count of messages for each supported message type: + +* NewRx +* RxChangeRequest +* RxChangeResponse +* CancelRx +* CancelRxResponse +* RxRenewalRequest +* RxRenewalResponse +* RxFill +* RxHistoryRequest +* RxHistoryResponse +* Status +* Error +* Verify + +The report will also include a count of outbound messages unable to be transmitted due to connectivity issues or other errors, for each message type. This report will be based on the contents of each client's local database table of stored messages. MIE will run the report for each client under consideration and aggregate the results. diff --git a/test/odt_md/issue-432.odt b/test/odt_md/issue-432.odt new file mode 100644 index 00000000..28b64ae4 Binary files /dev/null and b/test/odt_md/issue-432.odt differ diff --git a/test/odt_md/issue-434.md b/test/odt_md/issue-434.md new file mode 100644 index 00000000..4bfbf229 --- /dev/null +++ b/test/odt_md/issue-434.md @@ -0,0 +1,11 @@ +**DataVis**: An interactive spreadsheet-like grid showing data in columns and rows, which allows for filtering, sorting, and pivoting to obtain the view of the data desired. +**Type:** The order type is used to describe or categorize data into similar groupings. This allows the user to apply a filter to refine the search. +**Question Count**: Displays the number of AOE Questions associated with each row +**Order ID**: This is an internal, assigned number in the database and differs between systems +**Name:** The Name column shows the order name or description +**Code:** The Code is typically used within an interface transmission to clearly identify the order to the receiving Lab/interface engine, etc. +**Billing Code:** CPT or external code used for billing purposes +**LOINC Code:** Logical Observation Identifiers Names and Codes (LOINC) are used primarily for results coming into the system, to match "apples to apples". Enterprise Health's Medical Codify may be used to search for or reference LOINC codes, as well as other online resources. +**Layout:** Some orders have specific Exam Order Layouts assigned to them so they display consistently when results are manually entered, typically in the Tests and procedures section of an encounter. +**Doc Type:** A specific document type can be assigned to an order code within this editor, so that when these orders are resulted, a separate document is created. +**Appt. Type:** An appointment type can be tied to order codes within this editor so that when an order is on the due list and needs scheduled for an appointment, the system automatically selects the correct appointment type, with the proper amount of time allocated. diff --git a/test/odt_md/issue-434.odt b/test/odt_md/issue-434.odt new file mode 100644 index 00000000..6b791d92 Binary files /dev/null and b/test/odt_md/issue-434.odt differ diff --git a/test/odt_md/issue-435-436.md b/test/odt_md/issue-435-436.md new file mode 100644 index 00000000..026c682b --- /dev/null +++ b/test/odt_md/issue-435-436.md @@ -0,0 +1,13 @@ +## Editing Existing Questions + +Similar to adding a new question, users must have the proper permission to modify existing questions. +1. Locate the order to add questions to +2. Checkmark the order in the far left column +3. Click the ?Show Questions button +4. Locate the question to update and click on the row. + +![](10000001000001AE0000013A3DD8D81D5AC3DDAE.png) + +5. From here, you may update the Description, the process order, the question code, or the response. + 1. You may not change this from one "type" of response (like Text) to another type (like Yes/No). For this type of change, create a new question. + 2. Be sure to click the Submit button at the bottom to save your changes.![](10000001000003C90000010BE8F0122D00285531.png) diff --git a/test/odt_md/issue-435-436.odt b/test/odt_md/issue-435-436.odt new file mode 100644 index 00000000..6bf4da69 Binary files /dev/null and b/test/odt_md/issue-435-436.odt differ diff --git a/test/odt_md/list-indent.md b/test/odt_md/list-indent.md index 47918351..0c029d57 100644 --- a/test/odt_md/list-indent.md +++ b/test/odt_md/list-indent.md @@ -5,38 +5,38 @@ 3. When adding action items to panels, the [Representative Event panel action](#tyjcwt) is usually added to the panel first. Fill out all of the necessary fields according to the information acquired in the Health Surveillance matrix, and click Submit to save the panel action to the panel. - 1. Action Name: Required field. The Action Name is usually the name of a test/procedure that is the component/action of the panel. The name will be displayed listings and dialogues throughout the system. - 2. Lead Time: The Lead Time translates to the number of days prior to the Trigger Date the panel action becomes visible and is created within the system. This defines how many days before the Trigger Date that the panel/orders will populate on the Due List. Keep Lead Times consistent when setting multiple action items in a panel; otherwise, each component of the panel will have different Due Dates if there are different Lead Times on each. Emails can be configured to send email notifications, as needed, with a list of associated charts/employees that will be due. The recipient has the time between receiving the email and the panel action Trigger Date to notify Health Services of any issues or mistakes with the list. Emails to the member/chart will not be sent until the actual Trigger Date. (Email reminders are separately configured on a per client basis. Email notification may not apply to all clients). + 1. Action Name: Required field. The Action Name is usually the name of a test/procedure that is the component/action of the panel. The name will be displayed listings and dialogues throughout the system. + 2. Lead Time: The Lead Time translates to the number of days prior to the Trigger Date the panel action becomes visible and is created within the system. This defines how many days before the Trigger Date that the panel/orders will populate on the Due List. Keep Lead Times consistent when setting multiple action items in a panel; otherwise, each component of the panel will have different Due Dates if there are different Lead Times on each. Emails can be configured to send email notifications, as needed, with a list of associated charts/employees that will be due. The recipient has the time between receiving the email and the panel action Trigger Date to notify Health Services of any issues or mistakes with the list. Emails to the member/chart will not be sent until the actual Trigger Date. (Email reminders are separately configured on a per client basis. Email notification may not apply to all clients). {{% tip %}} - If the panel action is for a type of exposure, users will not want to set any Lead Time days. Lead Time is not needed for an exposure type panel action. - ![](1000020100000311000001824A182983854F26CB.png) + If the panel action is for a type of exposure, users will not want to set any Lead Time days. Lead Time is not needed for an exposure type panel action. + ![](1000020100000311000001824A182983854F26CB.png) {{% /tip %}} - 3. Required for Certification: Select this to indicate the panel action is required for members of the panel. Leave unchecked if the panel action is voluntary. If checked, a panel member failing or becoming overdue for the action will become de-certified from the panel. - 4. Indication Rule: Users can select any action rule found in the Action Rules editor, using the drop-down. For more information on the Action Rules, see the [Health Surveillance Action Rules](gdoc:10wTqIF8gtUDBbJmbk_LjlUeNmtU_vvbVFoVWTZnuMqc) documentation. The action rule must evaluate to True in order for this panel action to trigger for a panel member. [Action Rules](#1fob9te) are usually configured by an MIE Developer after an MIE Implementer has collected all of the necessary details for the configuration. - 1. Indication Rules can be used to only trigger the panel action for a member of the panel, if they are part of a specific department, for example. Or another more complex example would be a panel action configured to trigger a Hep3rd injection, only if the member of the panel had the second Hepatitis injection given within the last 8 weeks. - 5. Contraindication Rule: Users can select any action rule found in the Action Rules editor, using the drop-down. The action rule must evaluate to False in order for this panel action to trigger for a panel member. For more information on the Action Rules, see the [Health Surveillance Action Rules](gdoc:10wTqIF8gtUDBbJmbk_LjlUeNmtU_vvbVFoVWTZnuMqc) documentation. - 6. Trigger Type: Entry, Routine, Exit. Select the type of trigger, to define at what point in the panel member's current role/job status, the regulating agency or company requires the panel action to be completed. Entry will trigger when a panel member is first put in the panel. The Panel Evaluator scheduled job will run every day, triggering panels as appropriate, based on the the configured panel actions and the trigger type selected. - 7. Trigger Date: On what date should the panel action trigger? Use the drop-down to select one of the following Trigger Dates: - 1. Date of Birth: Triggers the panel action on the panel member's date of birth, on a schedule determined by the starting age and frequency. Assumes the panel member's DOB has been captured in the chart demographics. - 2. Other Action (Triggered): The Other Action (Triggered) trigger date allows users to trigger a panel action at the same time as another action item, indicated in this panel action. For example, an action to trigger an Audiogram may be for Entry, Routine, or Exit actions; if checked, other actions may use this panel action as a trigger. This option must be selected for the action to display in the Related Action list. The Related Action list displays when then Trigger Date is set to Other Action (Triggered) or Prior Action (Completed). Additionally, action items can be configured to trigger with the Representative Events, as needed, if that programming is utilized. This allows all action items to trigger together for a panel. Triggers with all the same date are usually tied to representative event. - - ![](100002010000033B00000036339CF669B6C2B512.png) - - 3. Point in Time: The Point in Time trigger date allows users to trigger an action item on the same day and month, each year (must be MM/DD format). - 4. Panel Expiration: Triggers on the expiration date specified in the panel status. Most panels will be configured with a representative event as the - 9. Trigger Others: If checked, other panel actions may use this action item as a trigger. This must be set for the action to display in the Related Action list. - 1. Auto-Waive (this action item) if none (no other actions) Triggered: In instances where a Representative Event may be added after the completion of all other panel actions - 10. Frequency: Day, Weeks, Months, Years. Use the drop-down to define the time period of how often the panel action should be triggered. Actions with zero (0) frequency values will trigger whenever the parent action is set to trigger. - 11. Valid For: Day, Weeks, Months, Years. Use the drop-down to define the acceptable time period for which the panel action may be performed, prior to the action Due Date, and still count as acceptable by the regulating body or company. - 1. Current Panel Only: This is a checkbox that is associated with the Valid For field. If checked, this panel action will be triggered, regardless of whether the same encounter or procedure was completed for a different panel. For example, if a panel member is included in both the Asbestos panel and Benzene panel, and both require a Chest Xray, then {{% system-name %}} would (by default) only populate Chest Xray once on the Due List. With the Current Panel Only option selected, in this example, the Chest Xray will display twice, once for each panel. - 12. Grace Period: Day, Weeks, Months, Years. Use the drop-down to define how much time the panel member is allotted to complete the panel action, from the time it is visible till the time it is considered overdue. Periodic email notifications can be set up with scheduled jobs, if preferred. The Grace Period is before the Due Date, meaning the Grace Period is the amount of time before the Due Date that the invitations, emails, and questionnaire become available. The panel member gets notified at the point of the Grace Period plus Lead Time. + 3. Required for Certification: Select this to indicate the panel action is required for members of the panel. Leave unchecked if the panel action is voluntary. If checked, a panel member failing or becoming overdue for the action will become de-certified from the panel. + 4. Indication Rule: Users can select any action rule found in the Action Rules editor, using the drop-down. For more information on the Action Rules, see the [Health Surveillance Action Rules](gdoc:10wTqIF8gtUDBbJmbk_LjlUeNmtU_vvbVFoVWTZnuMqc) documentation. The action rule must evaluate to True in order for this panel action to trigger for a panel member. [Action Rules](#1fob9te) are usually configured by an MIE Developer after an MIE Implementer has collected all of the necessary details for the configuration. + 1. Indication Rules can be used to only trigger the panel action for a member of the panel, if they are part of a specific department, for example. Or another more complex example would be a panel action configured to trigger a Hep3rd injection, only if the member of the panel had the second Hepatitis injection given within the last 8 weeks. + 5. Contraindication Rule: Users can select any action rule found in the Action Rules editor, using the drop-down. The action rule must evaluate to False in order for this panel action to trigger for a panel member. For more information on the Action Rules, see the [Health Surveillance Action Rules](gdoc:10wTqIF8gtUDBbJmbk_LjlUeNmtU_vvbVFoVWTZnuMqc) documentation. + 6. Trigger Type: Entry, Routine, Exit. Select the type of trigger, to define at what point in the panel member's current role/job status, the regulating agency or company requires the panel action to be completed. Entry will trigger when a panel member is first put in the panel. The Panel Evaluator scheduled job will run every day, triggering panels as appropriate, based on the the configured panel actions and the trigger type selected. + 7. Trigger Date: On what date should the panel action trigger? Use the drop-down to select one of the following Trigger Dates: + 1. Date of Birth: Triggers the panel action on the panel member's date of birth, on a schedule determined by the starting age and frequency. Assumes the panel member's DOB has been captured in the chart demographics. + 2. Other Action (Triggered): The Other Action (Triggered) trigger date allows users to trigger a panel action at the same time as another action item, indicated in this panel action. For example, an action to trigger an Audiogram may be for Entry, Routine, or Exit actions; if checked, other actions may use this panel action as a trigger. This option must be selected for the action to display in the Related Action list. The Related Action list displays when then Trigger Date is set to Other Action (Triggered) or Prior Action (Completed). Additionally, action items can be configured to trigger with the Representative Events, as needed, if that programming is utilized. This allows all action items to trigger together for a panel. Triggers with all the same date are usually tied to representative event. + + ![](100002010000033B00000036339CF669B6C2B512.png) + + 3. Point in Time: The Point in Time trigger date allows users to trigger an action item on the same day and month, each year (must be MM/DD format). + 4. Panel Expiration: Triggers on the expiration date specified in the panel status. Most panels will be configured with a representative event as the + 9. Trigger Others: If checked, other panel actions may use this action item as a trigger. This must be set for the action to display in the Related Action list. + 1. Auto-Waive (this action item) if none (no other actions) Triggered: In instances where a Representative Event may be added after the completion of all other panel actions + 10. Frequency: Day, Weeks, Months, Years. Use the drop-down to define the time period of how often the panel action should be triggered. Actions with zero (0) frequency values will trigger whenever the parent action is set to trigger. + 11. Valid For: Day, Weeks, Months, Years. Use the drop-down to define the acceptable time period for which the panel action may be performed, prior to the action Due Date, and still count as acceptable by the regulating body or company. + 1. Current Panel Only: This is a checkbox that is associated with the Valid For field. If checked, this panel action will be triggered, regardless of whether the same encounter or procedure was completed for a different panel. For example, if a panel member is included in both the Asbestos panel and Benzene panel, and both require a Chest Xray, then {{% system-name %}} would (by default) only populate Chest Xray once on the Due List. With the Current Panel Only option selected, in this example, the Chest Xray will display twice, once for each panel. + 12. Grace Period: Day, Weeks, Months, Years. Use the drop-down to define how much time the panel member is allotted to complete the panel action, from the time it is visible till the time it is considered overdue. Periodic email notifications can be set up with scheduled jobs, if preferred. The Grace Period is before the Due Date, meaning the Grace Period is the amount of time before the Due Date that the invitations, emails, and questionnaire become available. The panel member gets notified at the point of the Grace Period plus Lead Time. {{% note %}} - Health Questionnaires (if being done electronically and via portal) would be an Encounter event type and the specific electronic encounter order item would need selected (the order item that points to the electronic health questionnaire layout). For every questionnaire that users want documented electronically, via an encounter, two (2) order items and panel actions are needed; that's one (1) for the Health Questionnaire electronic encounter and the other (1) for the Due List item, in order to mark Complete. + Health Questionnaires (if being done electronically and via portal) would be an Encounter event type and the specific electronic encounter order item would need selected (the order item that points to the electronic health questionnaire layout). For every questionnaire that users want documented electronically, via an encounter, two (2) order items and panel actions are needed; that's one (1) for the Health Questionnaire electronic encounter and the other (1) for the Due List item, in order to mark Complete. {{% /note %}} - 13. Instructions: Free text instructions for a provider to perform this action item, if necessary. Could be instructions or pass/fail criteria, etc. + 13. Instructions: Free text instructions for a provider to perform this action item, if necessary. Could be instructions or pass/fail criteria, etc. diff --git a/test/odt_md/list-test.md b/test/odt_md/list-test.md index 1f89e138..b8636295 100644 --- a/test/odt_md/list-test.md +++ b/test/odt_md/list-test.md @@ -8,37 +8,37 @@ Action items that are configured with a Trigger Date of **Prior Action (Complet 3. level1_3 4. level1_4 - 1. level2_1 - 2. level2_2 - 3. level2_3 - 4. level2_4 + 1. level2_1 + 2. level2_2 + 3. level2_3 + 4. level2_4 {{% tip %}} - If the panel action is for a type of exposure, users will not want to set any Lead Time days. Lead Time is not needed for an exposure type panel action. + If the panel action is for a type of exposure, users will not want to set any Lead Time days. Lead Time is not needed for an exposure type panel action. {{% /tip %}} - 5. level2_5 - 6. level2_6 - 1. level3_1 - 7. level2_7 - 8. level2_8 - 9. level2_9 - 1. llevel3_1 - 2. llevel3_2 - 3. llevel3_3 - 4. llevel3_4 - 10. level2_10 - 1. lllevel3_1 - 11. level2_11 - 12. level2_12 - 1. llllevel3_1 - 13. level2_13 - 14. level2_14 + 5. level2_5 + 6. level2_6 + 1. level3_1 + 7. level2_7 + 8. level2_8 + 9. level2_9 + 1. llevel3_1 + 2. llevel3_2 + 3. llevel3_3 + 4. llevel3_4 + 10. level2_10 + 1. lllevel3_1 + 11. level2_11 + 12. level2_12 + 1. llllevel3_1 + 13. level2_13 + 14. level2_14 {{% note %}} - Health Questionnaires (if being done electronically and via portal) would be an Encounter event type and the specific electronic encounter order item would need selected (the order item that points to the electronic health questionnaire layout). For every questionnaire that users want documented electronically, via an encounter, two (2) order items and panel actions are needed; that's one (1) for the Health Questionnaire electronic encounter and the other (1) for the Due List item, in order to mark Complete. + Health Questionnaires (if being done electronically and via portal) would be an Encounter event type and the specific electronic encounter order item would need selected (the order item that points to the electronic health questionnaire layout). For every questionnaire that users want documented electronically, via an encounter, two (2) order items and panel actions are needed; that's one (1) for the Health Questionnaire electronic encounter and the other (1) for the Due List item, in order to mark Complete. {{% /note %}} - 15. level2_15 - 16. level2_16 - 17. level2_17 + 15. level2_15 + 16. level2_16 + 17. level2_17 diff --git a/test/odt_md/project-overview.md b/test/odt_md/project-overview.md index c39219d5..9895be89 100644 --- a/test/odt_md/project-overview.md +++ b/test/odt_md/project-overview.md @@ -243,11 +243,11 @@ The index is a listing of all of the defined terms and their references in the d ## Markdown Cleanup * Bold headings: ([issue](https://github.com/mieweb/wikiGDrive/issues/17)) Remove the ** bold markdown from all headings. - +![](10000201000001A5000000492C856905A808045C.png) * End of line bold text: ([issue](https://github.com/mieweb/wikiGDrive/issues/15)) The closing ** for bold text at the end of a line is being placed on a newline and not being parsed. - +![](10000201000005480000004BB83F3F8B5F0C77BD.png) * Italics/bold in an unordered list: ([issue](https://github.com/mieweb/wikiGDrive/issues/16)) Italics are not being rendered if in a list item. We may need to find these and replace the */** with em/strong tags. Example is rendered in browser next to [Google Doc](gdoc:108WScoxxGKKKOsGWF7UNZ4rLRanGXu6BPdJ-axjVn5s). - +![](1000020100000243000000F28AB7617254FDBB3A.png) ## Images diff --git a/website/_index.md b/website/_index.md index 426da708..0a36593d 100644 --- a/website/_index.md +++ b/website/_index.md @@ -1,5 +1,6 @@ --- #layout: overridden inside /hugo/themes/wgd-bootstrap/layouts/index.html +title: _index --- # wikiGDrive @@ -15,7 +16,7 @@ WikiGDrive is a node app that uses the [Google Drive API](https://developers.goo [Google Drive Notes](https://docs.google.com/document/d/1H6vwfQXIexdg4ldfaoPUjhOZPnSkNn6h29WD6Fi-SBY/edit#) | [Github Project](https://github.com/mieweb/wikiGDrive/projects) -| [Github Developer Notes](docs/developer_guide.md) +| [Github Developer Notes](docs/developer-guide.md) With a "Shared Drive" as the key, WikiGDrive: @@ -30,7 +31,7 @@ WikiGDrive scans for changes in the drive and then refresh the local converted f ## Developer Documentation -* [Developer README](docs/developer_guide.md) +* [Developer README](docs/developer-guide.md) * [Internals](docs/internals.md) ## Install from NPM diff --git a/website/docs/_index.md b/website/docs/_index.md index 7b939729..1e07b3a5 100644 --- a/website/docs/_index.md +++ b/website/docs/_index.md @@ -6,17 +6,13 @@ navWeight: 1000 # Upper weight gets higher precedence, optional. Google Drive to MarkDown synchronization -[![Develop Server Deploy](https://github.com/mieweb/wikiGDrive/actions/workflows/DevelopServerDeploy.yml/badge.svg?branch=develop&event=push)](https://github.com/mieweb/wikiGDrive/actions/workflows/DevelopServerDeploy.yml) -[![Prod Server Deploy](https://github.com/mieweb/wikiGDrive/actions/workflows/ProdServerDeploy.yml/badge.svg?branch=master&event=push)](https://github.com/mieweb/wikiGDrive/actions/workflows/ProdServerDeploy.yml) -[![CodeQL](https://github.com/mieweb/wikiGDrive/actions/workflows/codeql-analysis.yml/badge.svg?branch=master&event=push)](https://github.com/mieweb/wikiGDrive/actions/workflows/codeql-analysis.yml?query=event%3Apush+branch%3Amaster+) - WikiGDrive is a node app that uses the [Google Drive API](https://developers.google.com/drive/api/v3/quickstart/nodejs) to transform Google Docs and Drawings into markdown. ![Diagram](./diagram.svg) [Google Drive Notes](https://docs.google.com/document/d/1H6vwfQXIexdg4ldfaoPUjhOZPnSkNn6h29WD6Fi-SBY/edit#) | [Github Project](https://github.com/mieweb/wikiGDrive/projects) -| [Github Developer Notes](./developer_guide.md) +| [Github Developer Notes](./developer-guide.md) With a "Shared Drive" as the key, WikiGDrive: @@ -31,7 +27,7 @@ WikiGDrive scans for changes in the drive and then refresh the local converted f ## Developer Documentation -* [Developer README](./developer_guide.md) +* [Developer README](./developer-guide.md) * [Internals](./internals.md) ## Usage and options diff --git a/website/docs/developer_guide.md b/website/docs/developer-guide.md similarity index 100% rename from website/docs/developer_guide.md rename to website/docs/developer-guide.md diff --git a/website/docs/internals.md b/website/docs/internals.md index a925e37d..a7b10c38 100644 --- a/website/docs/internals.md +++ b/website/docs/internals.md @@ -156,9 +156,9 @@ navWeight: -15 2. If file is removed - remove .md file, remove images 3. If file is new (not exists in local_files.json) - add to localFiles, schedule for generation 4. If file exists but with different desireLocalPath: - * Remove old .md, remove old images - * Schedule for generation - * Generate redir with old localPath + * Remove old .md, remove old images + * Schedule for generation + * Generate redir with old localPath 5. Remove dangling redirects 6. Check if there are any conflicts (same desireLocalPath) 7. Check if any conflicts can be removed diff --git a/website/docs/usage/_index.md b/website/docs/usage/_index.md index a66906b7..27dbf166 100644 --- a/website/docs/usage/_index.md +++ b/website/docs/usage/_index.md @@ -1,5 +1,5 @@ --- -title: Usage +title: _index --- # Usage diff --git a/website/docs/usage/wikigdrive-usage.md b/website/docs/usage/wikigdrive-usage.md new file mode 100644 index 00000000..38ed8bde --- /dev/null +++ b/website/docs/usage/wikigdrive-usage.md @@ -0,0 +1,129 @@ +--- +title: wikigdrive usage +--- +# Usage + +## wikigdrive usage + +``` +$ wikigdrive [args] [] +``` + +Main commands: + +``` +wikigdrive config + --client_id + --client_secret + --service_account=./private_key.json + +wikigdrive server + --link_mode [mdURLs|dirURLs|uglyURLs] + +wikigdrive register [drive_id_or_url] + --drive [shared drive url] + --workdir (current working folder) + +wikigdrive pull [URL to specific file] + +wikigdrive watch (keep scanning for changes, ie: daemon) +``` + +Other commands: + +``` +wikigdrive status [ID of document] - Show status of the document or stats of the entire path. +wikigdrive drives +wikigdrive sync +wikigdrive download +wikigdrive transform +``` + +Examples: + +``` +$ wikigdrive init +$ wikigdrive add https://google.drive... +``` + +## wikigdrive config usage + +``` +$ wikigdrive config [] +``` + +Stores authentication config inside /auth_config.json to avoid specifying authentication options each time + +Options: + +``` +--client_id GOOGLE_DRIVE_API CLIENT_ID +--client_secret GOOGLE_DRIVE_API CLIENT_SECRET +--service_account GOOGLE_DRIVE_API SERVICE_ACCOUNT_JSON file location +``` + +## wikigdrive drives usage + +``` +$ wikigdrive drives [] +``` + +Displays drives available to user or service account + +Examples: + +``` +wikigdrive drives --client_id=AAA --client_secret=BBB +wikigdrive drives --service_account=./private_key.json +``` + +## wikigdrive server usage + +``` +$ wikigdrive server [] +``` + +Starts wikigdrive in multiuser server mode + +Options: + +``` +--share_email Email to share drives with +--server_port Server port (default 3000) +``` + +Examples: + +``` +$ wikigdrive server --share_email=example@example.com --server_port=3000 +``` + +## All commands + +Other commands: + +``` +wikigdrive status +wikigdrive drives +wikigdrive sync +wikigdrive download +wikigdrive transform +``` + +## Common options + +Options available for each command: + +### Data location + +``` +--workdir (current working folder) +``` + +### Authentication + +``` +--client_id GOOGLE_DRIVE_API CLIENT_ID +--client_secret GOOGLE_DRIVE_API CLIENT_SECRET +--service_account GOOGLE_DRIVE_API SERVICE_ACCOUNT_JSON file location +``` diff --git a/website/docs/usage/wikigdrive_usage.md b/website/docs/usage/wikigdrive_usage.md deleted file mode 100644 index 0e56b983..00000000 --- a/website/docs/usage/wikigdrive_usage.md +++ /dev/null @@ -1,101 +0,0 @@ ---- -title: wikigdrive ---- -# Usage - -## wikigdrive usage - - $ wikigdrive [args] [] - -Main commands: - - wikigdrive config - --client_id - --client_secret - --service_account=./private_key.json - - wikigdrive server - --link_mode [mdURLs|dirURLs|uglyURLs] - - wikigdrive register [drive_id_or_url] - --drive [shared drive url] - --workdir (current working folder) - - wikigdrive pull [URL to specific file] - - wikigdrive watch (keep scanning for changes, ie: daemon) - -Other commands: - - wikigdrive status [ID of document] - Show status of the document or stats of the entire path. - wikigdrive drives - wikigdrive sync - wikigdrive download - wikigdrive transform - -Examples: - - $ wikigdrive init - $ wikigdrive add https://google.drive... - -## wikigdrive config usage - - $ wikigdrive config [] - -Stores authentication config inside /auth_config.json to avoid specifying authentication options each time - -Options: - - --client_id GOOGLE_DRIVE_API CLIENT_ID - --client_secret GOOGLE_DRIVE_API CLIENT_SECRET - --service_account GOOGLE_DRIVE_API SERVICE_ACCOUNT_JSON file location - -## wikigdrive drives usage - - $ wikigdrive drives [] - -Displays drives available to user or service account - -Examples: - - wikigdrive drives --client_id=AAA --client_secret=BBB - wikigdrive drives --service_account=./private_key.json - -## wikigdrive server usage - - $ wikigdrive server [] - -Starts wikigdrive in multiuser server mode - -Options: - - --share_email Email to share drives with - --server_port Server port (default 3000) - -Examples: - - $ wikigdrive server --share_email=example@example.com --server_port=3000 - -## All commands - -Other commands: - - wikigdrive status - wikigdrive drives - wikigdrive sync - wikigdrive download - wikigdrive transform - -## Common options - -Options available for each command: - -### Data location - - --workdir (current working folder) - -### Authentication - - --client_id GOOGLE_DRIVE_API CLIENT_ID - --client_secret GOOGLE_DRIVE_API CLIENT_SECRET - --service_account GOOGLE_DRIVE_API SERVICE_ACCOUNT_JSON file location diff --git a/website/docs/usage/wikigdrivectl-usage.md b/website/docs/usage/wikigdrivectl-usage.md new file mode 100644 index 00000000..66e4dda2 --- /dev/null +++ b/website/docs/usage/wikigdrivectl-usage.md @@ -0,0 +1,41 @@ +--- +title: wikigdrivectl usage +--- +# Usage + +Command wikigdrivectl is used to control locally running wikigdrive server. + +## wikigdrivectl usage + +``` +$ wikigdrivectl [args] [] +``` + +Main commands: + +``` +wikigdrivectl ps +wikigdrivectl inspect [drive_id_or_url] +``` + +## wikigdrivectl ps + +``` +$ wikigdrivectl ps +``` + +Displays all drives with a job count + +## wikigdrivectl inspect + +``` +$ wikigdrivectl inspect [drive_id_or_url] +``` + +Displays jobs queue for specific drive + +Examples: + +``` +wikigdrivectl inspect http://drive.google.com/open?id=FOLDER_ID +``` diff --git a/website/docs/usage/wikigdrivectl_usage.md b/website/docs/usage/wikigdrivectl_usage.md deleted file mode 100644 index ac92d5bc..00000000 --- a/website/docs/usage/wikigdrivectl_usage.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: wikigdrivectl ---- -# Usage - -Command wikigdrivectl is used to control locally running wikigdrive server. - -## wikigdrivectl usage - - $ wikigdrivectl [args] [] - -Main commands: - - ps - inspect [drive_id_or_url] - -## wikigdrivectl ps - - $ wikigdrive ps - -Displays all drives with a job count - -## wikigdrivectl inspect - - $ wikigdrive inspect [drive_id_or_url] - -Displays jobs queue for specific drive - -Examples: - - wikigdrive inspect http://drive.google.com/open?id=FOLDER_ID diff --git a/website/ui.md b/website/ui.md deleted file mode 100644 index e8b5405e..00000000 --- a/website/ui.md +++ /dev/null @@ -1,3 +0,0 @@ ---- -# ui/index.html ----