From ea55754419b473e7c195ae0405276b7a368027c6 Mon Sep 17 00:00:00 2001 From: Kelly Joseph Price Date: Fri, 11 Oct 2024 12:29:34 -0700 Subject: [PATCH] feat: migrate emphasis (#12963) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit | 📖 [PR App][pr] | 🎫 Resolve CX-1135, RM-11085 | | :-------------: | :-----------------: | ## 🧰 Changes Fixes incompatable emphasis during the migration. I [fixed this](https://github.com/readmeio/markdown/pull/988) in `@readme/mdx`, but I think it'll be nicer to keep the migration specific stuff in the main app. ## 💁 Customer Impact ## 🧬 QA & Testing - [ ] insert the string `*the recommended initial action is to**initiate a[reversal operation (rollback)](https://docs.jupico.com/reference/ccrollback)**. *` into a page - [ ] note the bold and italics - [ ] migrate the project - [ ] confirm that the bold and italics render the same in the dash and hub - [Broken on next][next]. - [Working in this PR][pr]! [next]: https://next.readme.ninja [pr]: https://readme-pr-12963.readme.ninja [ui]: https://readme-pr-12963.readme.ninja/ui --- emphasis.ts | 55 +++++++++++++++++++++++++++ index.ts | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 emphasis.ts create mode 100644 index.ts diff --git a/emphasis.ts b/emphasis.ts new file mode 100644 index 000000000..fc27bd6a3 --- /dev/null +++ b/emphasis.ts @@ -0,0 +1,55 @@ +import type { $TSFixMe } from '@readme/iso'; +import type { Emphasis, Parent, Root, Strong, Text } from 'mdast'; + +import visit from 'unist-util-visit'; + +const strongTest = (node: any): node is Emphasis | Strong => ['emphasis', 'strong'].includes(node.type); + +const addSpaceBefore = (index: number, parent: Parent) => { + if (!(index > 0 && parent.children[index - 1])) return; + + const prev = parent.children[index - 1]; + // @ts-ignore - I think this is also a dependency versioning issue + if (!('value' in prev) || prev.value.endsWith(' ') || prev.type === 'escape') return; + + parent.children.splice(index, 0, { type: 'text', value: ' ' }); +}; + +const addSpaceAfter = (index: number, parent: Parent) => { + if (!(index < parent.children.length - 1 && parent.children[index + 1])) return; + + const nextChild = parent.children[index + 1]; + if (!('value' in nextChild) || nextChild.value.startsWith(' ')) return; + + parent.children.splice(index + 1, 0, { type: 'text', value: ' ' }); +}; + +const trimEmphasis = (node: Emphasis | Strong, index: number, parent: Parent) => { + let trimmed = false; + + // @ts-expect-error: the current version of visit is before the package + // types/mdast was created + visit(node, 'text', (child: Text) => { + const newValue = child.value.trim(); + + if (newValue !== child.value) { + trimmed = true; + child.value = newValue; + } + }); + + if (trimmed) { + addSpaceBefore(index, parent); + addSpaceAfter(index, parent); + } +}; + +const emphasisTransfomer = () => (tree: Root) => { + // @ts-expect-error: the current version of visit is before the package + // types/mdast was created + visit(tree, strongTest, trimEmphasis as $TSFixMe); + + return tree; +}; + +export default emphasisTransfomer; diff --git a/index.ts b/index.ts new file mode 100644 index 000000000..c30a734c6 --- /dev/null +++ b/index.ts @@ -0,0 +1,107 @@ +import type { $TSFixMe } from '@readme/iso'; +import type { Code, InlineCode, Root, Table, TableCell, TableRow } from 'mdast'; +import type { VFile } from 'vfile'; + +import * as rdmd from '@readme/markdown'; +import visit, { SKIP } from 'unist-util-visit'; + +import emphasisTransfomer from './emphasis'; + +const magicIndex = (i: number, j: number) => `${i === 0 ? 'h' : `${i - 1}`}-${j}`; + +// @note: This regex is detect malformed lists that were created by the +// markdown editor. Consider the following markdown: +// +// ``` +// * item 1 +// * item 2 +// * item 3 +// ``` +// +// This is a perfectly valid list. But when you put that text into a table +// cell, the editor does **bad** things. After a save and load cycle, it gets +// converted to this: +// +// ``` +// \_ item 1 +// \_ item 2 +// \* item 3 +// ``` +// +// The following regex attempts to detect this pattern, and we'll convert it to +// something more standard. +const psuedoListRegex = /^(?[ \t]*)\\?[*_]\s*(?.*)$/gm; + +const migrateTableCells = (vfile: VFile) => (table: Table) => { + let json; + try { + const { position } = table; + + if (position) { + json = JSON.parse( + vfile + .toString() + .slice(position.start.offset, position.end.offset) + .replace(/.*\[block:parameters\](.*)\[\/block\].*/s, '$1'), + ); + } + } catch (err) { + /** + * This failure case is already handled by the following logic. Plus, + * because it's being handled internally, there's no way for our + * migration script to catch the error or keep track of it, and it just + * ends up blowing up the output logs. + */ + // console.error(err); + } + + // @ts-expect-error: the current version of visit is before the package + // types/mdast was created + visit(table, 'tableRow', (row: TableRow, i: number) => { + // @ts-expect-error: the current version of visit is before the package + // types/mdast was created + visit(row, 'tableCell', (cell: TableCell, j: number) => { + let children = cell.children; + + if (json && json.data[magicIndex(i, j)]) { + const string = json.data[magicIndex(i, j)].replace(psuedoListRegex, '$1- $2'); + + children = rdmd.mdast(string).children; + } + + // eslint-disable-next-line no-param-reassign + cell.children = children.length > 1 ? children : ([{ type: 'paragraph', children }] as $TSFixMe); + + return SKIP; + }); + + return SKIP; + }); + + // @ts-expect-error: the current version of visit is before the package + // types/mdast was created + visit(table, 'inlineCode', (code: Code | InlineCode) => { + if (code.value.includes('\n')) { + // eslint-disable-next-line no-param-reassign + code.type = 'code'; + } + }); +}; + +const compatability = + () => + (tree: Root, vfile: VFile): Root => { + // @ts-expect-error: the current version of visit is before the package + // types/mdast was created + visit(tree, 'table', migrateTableCells(vfile)); + + return tree; + }; + +export const compatParser = (doc: string): Root => { + const proc = rdmd.processor().use(compatability).use(emphasisTransfomer); + const tree = proc.parse(doc); + proc.runSync(tree, doc); + + return tree; +};