-
Notifications
You must be signed in to change notification settings - Fork 166
Add normalize-yaml package to normalize YAML consistently #5066
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| #!/usr/bin/env node | ||
| import { promises as fsPromises } from 'fs'; | ||
| import { join } from 'path'; | ||
| import prettier from 'prettier'; | ||
| import normalize from './index.js'; | ||
|
|
||
| const { readFile, writeFile } = fsPromises; | ||
|
|
||
| /** @type {Record<string,any>=} */ | ||
| const prettierConfig = prettier.resolveConfig.sync(process.cwd()); | ||
|
|
||
| const files = process.argv.slice(2); | ||
| Promise.all( | ||
| files.map(async (relativePath) => { | ||
| const absolutePath = join(process.cwd(), relativePath); | ||
| const content = await readFile(absolutePath, 'utf8'); | ||
| await writeFile(absolutePath, normalize(content, prettierConfig)); | ||
| }), | ||
| ); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import YAML from 'yaml'; | ||
| import prettier from 'prettier'; | ||
| import visitors from './visitors/index.js'; | ||
|
|
||
| /** | ||
| * @param {string} content Original content. | ||
| * @param {Record<string,any>=} prettierConfig Optional Prettier configuration object. | ||
| * | ||
| * @return {string} Normalized content. | ||
| */ | ||
| function normalize(content, prettierConfig) { | ||
| const document = YAML.parseDocument(content); | ||
| YAML.visit(document, visitors); | ||
| return prettier.format(document.toString(), { ...prettierConfig, parser: 'yaml' }); | ||
| } | ||
|
|
||
| export default normalize; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import normalize from './index.js'; | ||
aduth marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| describe('normalize', () => { | ||
| it('applies smart quotes', () => { | ||
| const original = '---\na: \'<strong class="example...">Hello "world"...</strong>\''; | ||
| const expected = '---\na: \'<strong class="example...">Hello “world”…</strong>\'\n'; | ||
|
|
||
| expect(normalize(original)).to.equal(expected); | ||
| }); | ||
|
|
||
| it('retains comments', () => { | ||
| const original = '---\n# Comment\na: true'; | ||
| const expected = '---\n# Comment\na: true\n'; | ||
|
|
||
| expect(normalize(original)).to.equal(expected); | ||
| }); | ||
|
|
||
| it('sorts keys', () => { | ||
| const original = '---\nmap:\n b: false\n a: true'; | ||
| const expected = '---\nmap:\n a: true\n b: false\n'; | ||
|
|
||
| expect(normalize(original)).to.equal(expected); | ||
| }); | ||
|
|
||
| it('formats using prettier', () => { | ||
| const original = "---\nfoo: 'bar' "; | ||
| const expected = '---\nfoo: "bar"\n'; | ||
| const prettierConfig = { singleQuote: false }; | ||
|
|
||
| expect(normalize(original, prettierConfig)).to.equal(expected); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| { | ||
| "name": "@18f/identity-normalize-yaml", | ||
| "private": true, | ||
| "version": "1.0.0", | ||
| "type": "module", | ||
| "bin": { | ||
| "normalize-yaml": "./cli.js" | ||
| }, | ||
| "dependencies": { | ||
| "prettier": "^2.2.1", | ||
| "smartquotes": "^2.3.2", | ||
| "yaml": "^2.0.0-5" | ||
| } | ||
| } | ||
|
Comment on lines
+1
to
+14
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is there anything we can do that marks this as "please don't import me into the browser, I'm just meant for development"?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It's not something I've seen. Usually it's a judgment call from the consumer, and bundlers would often go out of their way to support packages written for a Node setting to be bundled for a browser (e.g. Webpack). This package could run in a browser, AFAICT, even if the bundle size would be pretty large. If we really wanted to, we could set a |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import smartquotes from 'smartquotes'; | ||
|
|
||
| /** | ||
| * @param {string} html String potentially containing HTML. | ||
| * @param {(substring: string) => string} replacer Replacer function given content between non-tag | ||
| * content to replace. | ||
| * | ||
| * @return {string} | ||
| */ | ||
| export function replaceInHTMLContent(html, replacer) { | ||
| return html.replace( | ||
| /([^<]*)(<.*?>)/g, | ||
| (_match, text, tag) => (text ? replacer(text) : text) + tag, | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Replaces any instance of three dot characters with single ellipsis characters. | ||
| * | ||
| * @param {string} string | ||
| * | ||
| * @return {string} | ||
| */ | ||
| export const ellipses = (string) => string.replace(/\.\.\./g, '…'); | ||
|
|
||
| export default /** @type {import('yaml').visitor} */ ({ | ||
| Scalar(_key, node) { | ||
| if (typeof node.value === 'string') { | ||
| node.value = replaceInHTMLContent(node.value, (string) => ellipses(smartquotes(string))); | ||
| } | ||
| }, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import { replaceInHTMLContent, ellipses } from './format-content.js'; | ||
|
|
||
| describe('replaceInHTMLContent', () => { | ||
| it('replaces in html content', () => { | ||
| const string = '<div>This is a failure</div>'; | ||
| const result = replaceInHTMLContent(string, (match) => match.replace('failure', 'success')); | ||
| const expected = '<div>This is a success</div>'; | ||
|
|
||
| expect(result).to.equal(expected); | ||
| }); | ||
|
|
||
| it('does not replace in html tags', () => { | ||
| const string = '<div data-div-type="div">div<div>div</div>div</div>'; | ||
| const result = replaceInHTMLContent(string, (match) => match.replace('div', '')); | ||
| const expected = '<div data-div-type="div"><div></div></div>'; | ||
|
|
||
| expect(result).to.equal(expected); | ||
| }); | ||
| }); | ||
|
|
||
| describe('ellipses', () => { | ||
| it('replaces all instances of dots', () => { | ||
| const string = 'You must first... before you can...'; | ||
| const result = ellipses(string); | ||
| const expected = 'You must first… before you can…'; | ||
|
|
||
| expect(result).to.equal(expected); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import formatContent from './format-content.js'; | ||
| import sortKeys from './sort-keys.js'; | ||
|
|
||
| export default /** @type {import('yaml').visitor} */ ({ | ||
| ...formatContent, | ||
| ...sortKeys, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| export default /** @type {import('yaml').visitor} */ ({ | ||
| Map(_key, node) { | ||
| node.items.sort( | ||
| /** | ||
| * @param {import('yaml').Pair<any>} a | ||
| * @param {import('yaml').Pair<any>} b | ||
| * @return {number} | ||
| */ | ||
| (a, b) => a.key.toString().localeCompare(b.key.toString()), | ||
| ); | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Toyed with supporting piped input to avoid the multi-step
make update_country_dialing_codes. Works, but keeping as-is since it's a bit simpler.