Skip to content

Commit

Permalink
fix: base the implementation on patching the parse method instead of …
Browse files Browse the repository at this point in the history
…preprocess

Using a regex to check for lines starting with `#` was dumb because it completely ignore the existence of code blocks. Now only "heading" tokens of the AST get modified which is much more robust.
  • Loading branch information
simonhaenisch committed Aug 28, 2024
1 parent d4331d6 commit 3d56058
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 56 deletions.
89 changes: 36 additions & 53 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,70 +5,53 @@ import { parsers } from 'prettier/plugins/markdown';
import { titleCase } from 'title-case';

/**
* Set `convertHeadingsToTitleCase` as the given parser's `preprocess` hook, or merge it with the existing one.
*
* @param {import('prettier').Parser} parser prettier parser
*/
const withTitleCasePreprocess = (parser) => {
return {
...parser,
/**
* @param {string} code
* @param {import('prettier').ParserOptions} options
*/
preprocess: (code, options) =>
convertHeadingsToTitleCase(
parser.preprocess ? parser.preprocess(code, options) : code,
options,
),
};
};

/**
* Convert all headings to title case using the `title-case` package.
* Call the given parser to get the AST, then convert the text values of all heading tokens to title-case.
*
* @param {string} code
* @param {import('prettier').ParserOptions} options
* @param {import('prettier').Parser} parser
*/
const convertHeadingsToTitleCase = (code, options) => {
try {
const titleCaseOptions = options.titleCase
? JSON.parse(options.titleCase)
: undefined;
async function parseWithHeadingsToTitleCase(code, options, parser) {
const titleCaseOptions = options.titleCase
? JSON.parse(options.titleCase)
: undefined;

return code
.split('\n')
.map((line) => {
if (!line.startsWith('#')) {
return line;
}
const ast = await parser.parse(code, options);

const content = line.replace(/^#+/, '').trim();
const headings = ast.children.filter((token) => token.type === 'heading');

const inlineCodeMatches = Array.from(content.matchAll(/`.+?`/g));
for (const heading of headings) {
const textTokens = heading.children.filter(
(token) => token.type === 'text',
);

let newContent = titleCase(content, titleCaseOptions);
const text = textTokens.map((token) => token.value).join('');

for (const match of inlineCodeMatches) {
const [inlineCode] = match;
let converted = titleCase(text, titleCaseOptions);

newContent =
newContent.slice(0, match.index) +
inlineCode +
newContent.slice(match.index + inlineCode.length);
}
textTokens.forEach((token) => {
token.value = converted.slice(0, token.value.length);
converted = converted.slice(token.value.length);
});
}

return line.replace(content, newContent);
})
.join('\n');
} catch (error) {
if (process.env.DEBUG) {
console.error(error);
}
return ast;
}

return code;
}
};
/**
* Patch the `parse` method of the given parser to use `parseWithHeadingsToTitleCase` instead which wraps the given parser.
*
* @param {import('prettier').Parser} parser
*
* @returns {import('prettier').Parser}
*/
function withPatchedParse(parser) {
return {
...parser,
parse: (code, options) =>
parseWithHeadingsToTitleCase(code, options, parser),
};
}

/**
* @type {import('prettier').Plugin}
Expand All @@ -84,6 +67,6 @@ export default {
},
},
parsers: {
markdown: withTitleCasePreprocess(parsers.markdown),
markdown: withPatchedParse(parsers.markdown),
},
};
32 changes: 32 additions & 0 deletions index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,35 @@ it('passes title-case options from the prettier config', async (t) => {

t.is(formattedCode, '# Some sentence - Only affects the first word!\n');
});

const fullExample = `
# foo bar \`inline-code\` baz
lorem ipsum
\`\`\`yaml
# some yaml comment
foo: bar
\`\`\`
## heading two
`.trimStart();

const fullExampleExpected = `
# Foo Bar \`inline-code\` Baz
lorem ipsum
\`\`\`yaml
# some yaml comment
foo: bar
\`\`\`
## Heading Two
`.trimStart();

it('works with the full example', async (t) => {
const formattedCode = await prettify(fullExample);

t.is(formattedCode, fullExampleExpected);
});
6 changes: 3 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ A plugin that makes Prettier convert your markdown headings to title case using

**Caveat**

This plugin inherits, extends, and then overrides the built-in Prettier parser for `markdown`. This means that it is incompatible with other plugins that do the same; only the last loaded plugin that exports one of those parsers will function.
This plugin overrides the built-in Prettier parser for `markdown`. This means that it is incompatible with other plugins that do the same; only the last loaded plugin that exports one of those parsers will function.

## Installation

Expand All @@ -37,7 +37,7 @@ export default {
};
```

Any line starting with `#` will be considered a heading. Doesn't work for inline-HTML.
It doesn't support inline HTML headings.

## Configuration

Expand All @@ -62,7 +62,7 @@ If it doesn't work, you can try to prefix your `prettier` command with `DEBUG=tr

## Rationale/Disclaimer

This plugin acts outside of [Prettier's scope](https://prettier.io/docs/en/rationale#what-prettier-is-_not_-concerned-about) because _"Prettier only prints code. It does not transform it."_, and technically converting the case is a code transformation. In my opinion however, Markdown is just markup and not really code, and it doesn't change the AST of the Markdown file (just the contents of some nodes). Therefore the practical benefits outweigh sticking with the philosophy in this case.
This plugin acts outside of [Prettier's scope](https://prettier.io/docs/en/rationale#what-prettier-is-_not_-concerned-about) because _"Prettier only prints code. It does not transform it."_, and technically converting the case is a code transformation. In my opinion however, Markdown is just markup and not really code, and it doesn't change the AST of the Markdown file (just the content of some text node values). Therefore the practical benefits outweigh sticking with the philosophy in this case.

## License

Expand Down

0 comments on commit 3d56058

Please sign in to comment.