Skip to content

Commit

Permalink
Add code linting and CI checks (#136)
Browse files Browse the repository at this point in the history
Add Prettier + ESLint to handle code formatting automatically and warn about anything else that might be an issue. This also uses `eslint-plugin-compat` to try and ensure we don't accidentally use APIs that aren't in all the runtimes we want to support (it's not perfect, but it helps).
  • Loading branch information
Mr0grog authored Dec 15, 2023
1 parent bd6283e commit b5f6961
Show file tree
Hide file tree
Showing 27 changed files with 1,585 additions and 500 deletions.
5 changes: 5 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

[*.js]
max_line_length = 120

[*.md]
indent_size = 4

[test/fixtures/**/*.md]
trim_trailing_whitespace = false
71 changes: 71 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{
"plugins": ["compat", "@stylistic/js"],

"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module"
},

"env": {
"browser": true,
"es2022": true,
"node": true
},

"settings": {
"browsers": "> 0.5%, last 2 versions, Firefox ESR, node >= 16, not dead, not op_mini all",
"lintAllEsApis": true
},

"extends": ["eslint:recommended", "plugin:compat/recommended"],

"rules": {
"curly": ["error", "multi-line", "consistent"],
"eqeqeq": ["error", "always", { "null": "ignore" }],
"@stylistic/js/max-len": [
"error",
{
"code": 120,
"comments": 80,
"ignoreUrls": true,
"ignoreTemplateLiterals": true
}
],
"no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_"
}
]
},

"overrides": [
{
"files": ["scripts/**/*.js"],
"settings": {
"browsers": "node >= 16"
}
},
{
"files": ["test/**/*.js"],
"env": {
"mocha": true
}
},
{
"files": ["test/e2e/**/*.js"],
"settings": {
"browsers": "node >= 16"
}
},
{
"files": ["test/unit/**/*.js", "test/support/**/*.js"],
"excludedFiles": ["test/support/wdio-webpack-dev-server.js"],
"settings": {
"browsers": "> 0.5%, last 20 versions, Firefox ESR, not dead, not op_mini all"
}
}
],

"ignorePatterns": ["scratch.*", "dist/**/*"]
}
30 changes: 15 additions & 15 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: "weekly"
interval: 'weekly'

- package-ecosystem: "npm"
directory: "/"
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: "monthly"
interval: 'monthly'
versioning-strategy: widen
groups:
# WebdriverIO frequently makes cross-ecosystem changes
# that break unless updated together.
wdio:
patterns:
- "@wdio/*"
- "wdio-*"
- "webdriverio"
- '@wdio/*'
- 'wdio-*'
- 'webdriverio'
rehype:
patterns:
- "rehype-*"
- 'rehype-*'
remark:
patterns:
- "remark-*"
- 'remark-*'
unified:
patterns:
- "unified"
- "unist-*"
- 'unified'
- 'unist-*'
webpack:
patterns:
- "webpack"
- "webpack-*"
- 'webpack'
- 'webpack-*'
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,27 @@ on:
name: Continuous Integration

jobs:
lint:
name: Lint Code
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 16
cache: npm

- name: Install dependencies
run: npm ci

- name: ESLint
run: npx eslint .

- name: Prettier
run: npx prettier --check .

build:
runs-on: ubuntu-latest

Expand Down
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# System Files
.DS_Store

# Editor-specific configuration files. Different users might want a slightly
# different local dev setup.
.vscode/settings.json
.idea
*.iml

node_modules
dist
logs
Expand Down
20 changes: 20 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.DS_Store

node_modules
dist
logs
temp
coverage

# Ignore tool versioning files -- development and testing should not be hard on
# multiple versions of Node.js, so we don't want these in the repo, but it's
# fine (or even good!) for contributors to use them locally.
.node-version
.nvmrc
.tool-versions

/test/fixtures/

# Prettier generates invalid HTML.
*.html
*.md
6 changes: 6 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"quoteProps": "consistent",
"printWidth": 80,
"singleQuote": true,
"trailingComma": "es5"
}
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ First make sure you have Node.js installed. Then:
```sh
> npm run build
```

…and the built output will be in the `dist` folder.

To start a server with live rebuilding, run:

```sh
> npm start
```

Then point your browser to `http://localhost:9000` to see the site. It will automatically rebuild whenever you change any files.


Expand Down
20 changes: 10 additions & 10 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { convertDocsHtmlToMarkdown } from './lib/convert.js';
import debug from 'debug'
import debug from 'debug';

const SLICE_CLIP_MEDIA_TYPE = 'application/x-vnd.google-docs-document-slice-clip';
const SLICE_CLIP_MEDIA_TYPE =
'application/x-vnd.google-docs-document-slice-clip';

const log = debug('app:index:debug')
const log = debug('app:index:debug');

const inputElement = document.getElementById('input');
const outputElement = document.getElementById('output');
Expand All @@ -12,15 +13,15 @@ const outputInstructions = document.querySelector('#output-area .instructions');

// Hold most recently pasted Slice Clip (the Google Docs internal copy/paste
// format) globally so we can re-use it if the user hand-edits the input.
let latestSliceClip = null
inputElement.addEventListener('paste', event => {
let latestSliceClip = null;
inputElement.addEventListener('paste', (event) => {
if (!event.clipboardData) {
console.warn('Could not access clipboard data from paste event');
return;
}

// Allow for raw or wrapped slice clips (one uses a "+wrapped" suffix).
const sliceClipType = event.clipboardData.types.find(type =>
const sliceClipType = event.clipboardData.types.find((type) =>
type.startsWith(SLICE_CLIP_MEDIA_TYPE)
);
log('Slice clip media type: %s', sliceClipType);
Expand All @@ -36,11 +37,11 @@ inputElement.addEventListener('input', () => {
inputInstructions.style.display = hasContent ? 'none' : '';

convertDocsHtmlToMarkdown(inputElement.innerHTML, latestSliceClip)
.then(markdown => {
.then((markdown) => {
outputElement.value = markdown;
outputInstructions.style.display = markdown.trim() ? 'none' : '';
})
.catch(error => {
.catch((error) => {
console.error(error);
outputInstructions.style.display = '';
});
Expand Down Expand Up @@ -77,8 +78,7 @@ if (window.URL && window.File) {
link.download = file.name;
document.body.appendChild(link);
link.click();
}
finally {
} finally {
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
Expand Down
45 changes: 26 additions & 19 deletions lib/convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { unified } from 'unified';
/** @typedef {import("hast-util-to-mdast").Handle} Handle */

/** @type {Handle} */
function preserveTagAndConvertContents (state, node, _parent) {
function preserveTagAndConvertContents(state, node, _parent) {
return [
{ type: 'html', value: `<${node.tagName}>` },
...state.all(node),
Expand All @@ -22,13 +22,13 @@ function preserveTagAndConvertContents (state, node, _parent) {
}

/** @type {Handle} */
function headingWithId (state, node, _parent) {
const newNode = defaultHandlers[node.tagName](state, node)
function headingWithId(state, node, _parent) {
const newNode = defaultHandlers[node.tagName](state, node);

if (node.properties?.id) {
newNode.children?.push({
type: 'html',
value: `<a id="${node.properties.id}"></a>`
value: `<a id="${node.properties.id}"></a>`,
});
}

Expand All @@ -44,7 +44,7 @@ function headingWithId (state, node, _parent) {
* @param {MdastState} _state
* @returns {boolean|number|void}
*/
function doubleBlankLinesBeforeHeadings (previous, next, _parent, _state) {
function doubleBlankLinesBeforeHeadings(previous, next, _parent, _state) {
if (previous.type !== 'heading' && next.type === 'heading') {
return 2;
}
Expand All @@ -65,8 +65,8 @@ const processor = unified()
h3: headingWithId,
h4: headingWithId,
h5: headingWithId,
h6: headingWithId
}
h6: headingWithId,
},
})
.use(remarkGfm)
.use(stringify, {
Expand All @@ -75,7 +75,7 @@ const processor = unified()
fences: false,
listItemIndent: 'one',
strong: '*',
join: [doubleBlankLinesBeforeHeadings]
join: [doubleBlankLinesBeforeHeadings],
});

/**
Expand All @@ -88,27 +88,34 @@ const processor = unified()
*/
function parseGdocsSliceClip(raw) {
const wrapper = typeof raw === 'string' ? JSON.parse(raw) : raw;
const data = typeof wrapper.data === 'string' ? JSON.parse(wrapper.data) : wrapper.data;
const data =
typeof wrapper.data === 'string' ? JSON.parse(wrapper.data) : wrapper.data;

// Do a basic check to ensure we are dealing with what we think we are. This
// is not meant to be exhaustive or to check the exact schema.
if (
typeof data?.resolved?.dsl_entitypositionmap !== 'object'
|| typeof data?.resolved?.dsl_spacers !== 'string'
|| !Array.isArray(data?.resolved?.dsl_styleslices)
typeof data?.resolved?.dsl_entitypositionmap !== 'object' ||
typeof data?.resolved?.dsl_spacers !== 'string' ||
!Array.isArray(data?.resolved?.dsl_styleslices)
) {
throw new SyntaxError(`Document does not appear to be a GDocs Slice Clip: ${JSON.stringify(raw)}`);
throw new SyntaxError(
`Document does not appear to be a GDocs Slice Clip: ${JSON.stringify(
raw
)}`
);
}

return data;
}

export function convertDocsHtmlToMarkdown(html, rawSliceClip) {
const sliceClip = rawSliceClip ? parseGdocsSliceClip(rawSliceClip) : null;
return processor.process({
value: html,
data: {
sliceClip
}
}).then(result => result.value);
return processor
.process({
value: html,
data: {
sliceClip,
},
})
.then((result) => result.value);
}
Loading

0 comments on commit b5f6961

Please sign in to comment.