├── .eslintignore ├── .eslintrc.json ├── .github ├── CODEOWNERS ├── blunderbuss.yml ├── linters │ ├── .htmlhintrc │ ├── .yaml-lint.yml │ └── sun_checks.xml ├── sync-repo-settings.yaml └── workflows │ ├── automation.yml │ ├── docs.yml │ ├── lint.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .releaserc ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── assets ├── docs.css └── logo.jpg ├── e2e ├── __fixtures__ │ └── complete.json ├── __html__ │ ├── complete.json.html │ └── complete.json_styles_false.html ├── __snapshots__ │ ├── hast.test.ts.snap │ └── html.test.ts.snap ├── cases.ts ├── hast.test.ts └── html.test.ts ├── package-lock.json ├── package.json ├── src ├── hast │ ├── common │ │ ├── paragraphStyle.ts │ │ ├── style.test.ts │ │ ├── style.ts │ │ ├── wrapStyle.test.ts │ │ └── wrapStyle.ts │ ├── index.ts │ ├── lists.ts │ ├── paragraph │ │ ├── index.ts │ │ ├── inlineObject.ts │ │ ├── person.test.ts │ │ ├── person.ts │ │ ├── richLink.test.ts │ │ ├── richLink.ts │ │ └── textRun.ts │ ├── postProcessing │ │ ├── prettyHeaderIds.ts │ │ └── removeStyles.ts │ └── table │ │ └── index.ts └── index.ts ├── tsconfig.json └── typedoc.cjs /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | dist/ 3 | docs/ 4 | lib/ 5 | node_modules/ 6 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:jsdoc/recommended", 5 | "plugin:prettier/recommended", 6 | "plugin:@typescript-eslint/recommended" 7 | ], 8 | "parser": "@typescript-eslint/parser", 9 | "plugins": ["prettier", "@typescript-eslint", "import"], 10 | "env": { 11 | "node": true, 12 | "es6": true 13 | }, 14 | "root": true, 15 | "rules": { 16 | "@typescript-eslint/ban-ts-comment": "off", 17 | "@typescript-eslint/consistent-type-imports": [ 18 | "error", 19 | { 20 | "prefer": "type-imports" 21 | } 22 | ], 23 | "@typescript-eslint/no-unused-vars": "error", 24 | "import/order": [ 25 | "error", 26 | { 27 | "alphabetize": { 28 | "caseInsensitive": true, 29 | "order": "asc" 30 | }, 31 | "groups": [ 32 | "builtin", 33 | "external", 34 | "internal", 35 | ["sibling", "parent", "index"], 36 | "unknown", 37 | "type" 38 | ], 39 | "newlines-between": "always" 40 | } 41 | ], 42 | "prettier/prettier": "error", 43 | "jsdoc/require-param-type": "off", 44 | "jsdoc/require-returns-type": "off" 45 | 46 | }, 47 | "settings": { 48 | "jsdoc": { 49 | "ignoreInternal": true, 50 | "ignorePrivate": true 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners 16 | 17 | .github/ @googleworkspace/workspace-devrel-dpe 18 | -------------------------------------------------------------------------------- /.github/blunderbuss.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | assign_issues: 16 | - jpoehnelt 17 | assign_prs: 18 | - jpoehnelt 19 | -------------------------------------------------------------------------------- /.github/linters/.htmlhintrc: -------------------------------------------------------------------------------- 1 | { 2 | "tagname-lowercase": true, 3 | "attr-lowercase": true, 4 | "attr-value-double-quotes": true, 5 | "attr-value-not-empty": false, 6 | "attr-no-duplication": true, 7 | "doctype-first": false, 8 | "tag-pair": true, 9 | "tag-self-close": false, 10 | "spec-char-escape": false, 11 | "id-unique": true, 12 | "src-not-empty": true, 13 | "title-require": false, 14 | "alt-require": true, 15 | "doctype-html5": true, 16 | "id-class-value": false, 17 | "style-disabled": false, 18 | "inline-style-disabled": false, 19 | "inline-script-disabled": false, 20 | "space-tab-mixed-disabled": "space", 21 | "id-class-ad-disabled": false, 22 | "href-abs-or-rel": false, 23 | "attr-unsafe-chars": true, 24 | "head-script-disabled": false 25 | } 26 | -------------------------------------------------------------------------------- /.github/linters/.yaml-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ########################################### 3 | # These are the rules used for # 4 | # linting all the yaml files in the stack # 5 | # NOTE: # 6 | # You can disable line with: # 7 | # # yamllint disable-line # 8 | ########################################### 9 | rules: 10 | braces: 11 | level: warning 12 | min-spaces-inside: 0 13 | max-spaces-inside: 0 14 | min-spaces-inside-empty: 1 15 | max-spaces-inside-empty: 5 16 | brackets: 17 | level: warning 18 | min-spaces-inside: 0 19 | max-spaces-inside: 0 20 | min-spaces-inside-empty: 1 21 | max-spaces-inside-empty: 5 22 | colons: 23 | level: warning 24 | max-spaces-before: 0 25 | max-spaces-after: 1 26 | commas: 27 | level: warning 28 | max-spaces-before: 0 29 | min-spaces-after: 1 30 | max-spaces-after: 1 31 | comments: disable 32 | comments-indentation: disable 33 | document-end: disable 34 | document-start: 35 | level: warning 36 | present: true 37 | empty-lines: 38 | level: warning 39 | max: 2 40 | max-start: 0 41 | max-end: 0 42 | hyphens: 43 | level: warning 44 | max-spaces-after: 1 45 | indentation: 46 | level: warning 47 | spaces: consistent 48 | indent-sequences: true 49 | check-multi-line-strings: false 50 | key-duplicates: enable 51 | line-length: 52 | level: warning 53 | max: 120 54 | allow-non-breakable-words: true 55 | allow-non-breakable-inline-mappings: true 56 | new-line-at-end-of-file: disable 57 | new-lines: 58 | type: unix 59 | trailing-spaces: disable -------------------------------------------------------------------------------- /.github/linters/sun_checks.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 18 | 19 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 65 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 82 | 83 | 84 | 86 | 87 | 88 | 94 | 95 | 96 | 97 | 100 | 101 | 102 | 103 | 104 | 108 | 109 | 110 | 111 | 112 | 114 | 115 | 116 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 135 | 137 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 185 | 186 | 187 | 189 | 191 | 192 | 193 | 194 | 196 | 197 | 198 | 199 | 201 | 202 | 203 | 204 | 206 | 207 | 208 | 209 | 211 | 212 | 213 | 214 | 216 | 217 | 218 | 219 | 221 | 222 | 223 | 224 | 226 | 227 | 228 | 229 | 231 | 232 | 233 | 234 | 236 | 237 | 238 | 239 | 241 | 242 | 243 | 244 | 246 | 247 | 248 | 249 | 251 | 253 | 255 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 287 | 288 | 289 | 292 | 293 | 294 | 295 | 301 | 302 | 303 | 304 | 308 | 309 | 310 | 311 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 326 | 327 | 328 | 329 | 330 | 331 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 347 | 348 | 349 | 350 | 353 | 354 | 355 | 356 | 357 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 371 | 372 | 373 | 374 | -------------------------------------------------------------------------------- /.github/sync-repo-settings.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # .github/sync-repo-settings.yaml 16 | # See https://github.com/googleapis/repo-automation-bots/tree/main/packages/sync-repo-settings for app options. 17 | rebaseMergeAllowed: true 18 | squashMergeAllowed: true 19 | mergeCommitAllowed: false 20 | deleteBranchOnMerge: true 21 | branchProtectionRules: 22 | - pattern: main 23 | isAdminEnforced: false 24 | requiresStrictStatusChecks: false 25 | requiredStatusCheckContexts: 26 | # .github/workflows/test.yml with a job called "test" 27 | - "test" 28 | # .github/workflows/lint.yml with a job called "lint" 29 | - "lint" 30 | # Google bots below 31 | - "cla/google" 32 | - "snippet-bot check" 33 | - "header-check" 34 | - "conventionalcommits.org" 35 | requiredApprovingReviewCount: 1 36 | requiresCodeOwnerReviews: true 37 | permissionRules: 38 | - team: workspace-devrel-dpe 39 | permission: admin 40 | - team: workspace-devrel 41 | permission: push 42 | -------------------------------------------------------------------------------- /.github/workflows/automation.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | --- 15 | name: Automation 16 | on: [ push, pull_request, workflow_dispatch ] 17 | jobs: 18 | dependabot: 19 | runs-on: ubuntu-latest 20 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request' }} 21 | env: 22 | PR_URL: ${{github.event.pull_request.html_url}} 23 | GITHUB_TOKEN: ${{secrets.GOOGLEWORKSPACE_BOT_TOKEN}} 24 | steps: 25 | - name: approve 26 | run: gh pr review --approve "$PR_URL" 27 | - name: merge 28 | run: gh pr merge --auto --squash --delete-branch "$PR_URL" 29 | default-branch-migration: 30 | # this job helps with migrating the default branch to main 31 | # it pushes main to master if master exists and main is the default branch 32 | # it pushes master to main if master is the default branch 33 | runs-on: ubuntu-latest 34 | if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' }} 35 | steps: 36 | - uses: actions/checkout@v2 37 | with: 38 | fetch-depth: 0 39 | # required otherwise GitHub blocks infinite loops in pushes originating in an action 40 | token: ${{ secrets.GOOGLEWORKSPACE_BOT_TOKEN }} 41 | - name: Set env 42 | run: | 43 | # set DEFAULT BRANCH 44 | echo "DEFAULT_BRANCH=$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name')" >> "$GITHUB_ENV"; 45 | 46 | # set HAS_MASTER_BRANCH 47 | if [ -n "$(git ls-remote --heads origin master)" ]; then 48 | echo "HAS_MASTER_BRANCH=true" >> "$GITHUB_ENV" 49 | else 50 | echo "HAS_MASTER_BRANCH=false" >> "$GITHUB_ENV" 51 | fi 52 | env: 53 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | - name: configure git 55 | run: | 56 | git config --global user.name 'googleworkspace-bot' 57 | git config --global user.email 'googleworkspace-bot@google.com' 58 | - if: ${{ env.DEFAULT_BRANCH == 'main' && env.HAS_MASTER_BRANCH == 'true' }} 59 | name: Update master branch from main 60 | run: | 61 | git checkout -B master 62 | git reset --hard origin/main 63 | git push origin master 64 | - if: ${{ env.DEFAULT_BRANCH == 'master'}} 65 | name: Update main branch from master 66 | run: | 67 | git checkout -B main 68 | git reset --hard origin/master 69 | git push origin main 70 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Docs 16 | on: [push, pull_request] 17 | jobs: 18 | test: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: actions/cache@v3 23 | with: 24 | path: ~/.npm 25 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 26 | restore-keys: | 27 | ${{ runner.os }}-node- 28 | - run: | 29 | npm i 30 | npm run docs 31 | - uses: peaceiris/actions-gh-pages@v3 32 | if: github.ref == 'refs/heads/main' 33 | with: 34 | github_token: ${{ secrets.GITHUB_TOKEN }} 35 | publish_dir: ./docs 36 | user_name: 'googleworkspace-bot' 37 | user_email: 'googleworkspace-bot@users.noreply.github.com' 38 | commit_message: ${{ github.event.head_commit.message }} 39 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Lint 16 | on: [push, pull_request] 17 | jobs: 18 | lint: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: actions/cache@v3 23 | with: 24 | path: ~/.npm 25 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 26 | restore-keys: | 27 | ${{ runner.os }}-node- 28 | - run: npm i 29 | - run: npm run lint 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Release 16 | on: 17 | push: 18 | branches: 19 | - main 20 | concurrency: release 21 | jobs: 22 | release: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/setup-node@v3 26 | - uses: actions/checkout@v2 27 | with: 28 | token: ${{ secrets.GOOGLEWORKSPACE_BOT_TOKEN }} 29 | - uses: actions/cache@v3 30 | with: 31 | path: ~/.npm 32 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 33 | restore-keys: | 34 | ${{ runner.os }}-node- 35 | - name: Test 36 | run: | 37 | npm ci 38 | npm test 39 | - name: Release 40 | uses: cycjimmy/semantic-release-action@v4 41 | with: 42 | extra_plugins: | 43 | @semantic-release/commit-analyzer 44 | semantic-release-interval 45 | @semantic-release/release-notes-generator 46 | @semantic-release/git 47 | @semantic-release/github 48 | @semantic-release/npm 49 | conventional-changelog-conventionalcommits 50 | env: 51 | GH_TOKEN: ${{ secrets.GOOGLEWORKSPACE_BOT_TOKEN }} 52 | NPM_TOKEN: ${{ secrets.NPM_WOMBAT_TOKEN }} 53 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Test 16 | on: [push, pull_request] 17 | jobs: 18 | test: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: actions/cache@v3 23 | with: 24 | path: ~/.npm 25 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 26 | restore-keys: | 27 | ${{ runner.os }}-node- 28 | - run: npm ci 29 | - run: npm test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | docs 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | public 106 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | plugins: 3 | [ 4 | [ 5 | "@semantic-release/commit-analyzer", 6 | { releaseRules: [{ type: "docs", scope: "README", release: "patch" }] }, 7 | ], 8 | ["semantic-release-interval", { duration: 1, units: "month" }], 9 | [ 10 | "@semantic-release/release-notes-generator", 11 | { 12 | preset: "conventionalcommits", 13 | presetConfig: 14 | { 15 | types: 16 | [ 17 | { type: "feat", section: "Features" }, 18 | { type: "feature", section: "Features" }, 19 | { type: "fix", section: "Bug Fixes" }, 20 | { type: "perf", section: "Performance Improvements" }, 21 | { type: "revert", section: "Reverts" }, 22 | { type: "docs", section: "Documentation", hidden: false }, 23 | { type: "style", section: "Styles", hidden: false }, 24 | { 25 | type: "chore", 26 | section: "Miscellaneous Chores", 27 | hidden: false, 28 | }, 29 | { 30 | type: "refactor", 31 | section: "Code Refactoring", 32 | hidden: false, 33 | }, 34 | { type: "test", section: "Tests", hidden: false }, 35 | { type: "build", section: "Build System", hidden: false }, 36 | { 37 | type: "ci", 38 | section: "Continuous Integration", 39 | hidden: false, 40 | }, 41 | ], 42 | }, 43 | }, 44 | ], 45 | "@semantic-release/github", 46 | "@semantic-release/npm", 47 | "@semantic-release/git", 48 | ], 49 | options: { debug: true }, 50 | branches: 51 | [ 52 | "main", 53 | { name: "beta", prerelease: true }, 54 | { name: "alpha", prerelease: true }, 55 | ], 56 | } 57 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "liveServer.settings.port": 5501 3 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code Reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google.com/conduct/). 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](./assets/logo.jpg) 2 | 3 | [![npm](https://img.shields.io/npm/v/@googleworkspace/google-docs-hast)](https://www.npmjs.com/package/@googleworkspace/google-docs-hast) 4 | [![Test](https://github.com/googleworkspace/google-docs-hast/actions/workflows/test.yml/badge.svg)](https://github.com/googleworkspace/google-docs-hast/actions/workflows/test.yml) 5 | ![Release](https://github.com/googleworkspace/google-docs-hast/workflows/Release/badge.svg) 6 | [![Docs](https://img.shields.io/badge/documentation-api-brightgreen)](https://googleworkspace.github.io/google-docs-hast/) 7 | 8 | ## Description 9 | 10 | Converts the JSON representation of a Google Docs document into an [HTML abstract syntax tree (HAST)](https://github.com/syntax-tree/hast) which can be serialized to HTML or converted to Markdown. 11 | 12 | > **Note:** This library does **not** intend to match the rendering by Google Docs. 13 | 14 | ## Install 15 | 16 | Install using NPM or similar. 17 | 18 | ```sh 19 | npm i @googleworkspace/google-docs-hast 20 | ``` 21 | 22 | ## Usage 23 | 24 | ```js 25 | import { toHast } from "@googleworkspace/google-docs-hast"; 26 | 27 | // Retrieve document from API, https://developers.google.com/docs/api 28 | const doc = ...; 29 | 30 | // Convert the document to an HTML AST. 31 | const tree = toHast(doc); 32 | ``` 33 | 34 | To get the serialized representation of the HTML AST, use the [rehype-stringify](https://www.npmjs.com/package/rehype-stringify) package. 35 | 36 | ```js 37 | import { unified } from "unified"; 38 | import rehypeStringify from "rehype-stringify"; 39 | 40 | // Convert the document to an HTML string. 41 | const html = unified() 42 | .use(rehypeStringify, { collapseEmptyAttributes: true }) 43 | .stringify(tree); 44 | ``` 45 | 46 | ### Images 47 | 48 | All `` elements should be post-processed as the `src` attribute is only valid for a short time and is of the pattern `https://lh6.googleusercontent.com/...`. 49 | 50 | ```js 51 | import { visit } from "unist-util-visit"; 52 | 53 | visit(tree, (node) => { 54 | if (node.type === "element" && node.tagName === "img") { 55 | const { src } = node.properties; 56 | // download, store, and replace the src attribute 57 | node.properties.src = newSrc; 58 | } 59 | }); 60 | ``` 61 | 62 | ### Named styles 63 | 64 | Named styles are converted to an HTML element matching the following table. 65 | 66 | | Named Style | HTML | 67 | | ----------- | ----------------------------- | 68 | | Title | `

` | 69 | | Subtitle | `

` | 70 | | Heading 1 | `

` | 71 | | Heading 2 | `

` | 72 | | Heading 3 | `

` | 73 | | Heading 4 | `

` | 74 | | Heading 5 | `
` | 75 | | Heading 6 | `
` | 76 | | Normal Text | `

` | 77 | 78 | ### Text styles 79 | 80 | Text styles are converted to an HTML element: ``, ``, ``, ``, ``, and ``. 81 | 82 | If there is no direct mapping, a `` with CSS is used to support features such as text color and font. This can be disabled with `{ styles: false }`. 83 | 84 | ### Anchor links 85 | 86 | Header IDs are in the form `id="h.wn8l66qm9m7y"` when exported from the Google Docs API. By default, header tag IDs are updated to match their text content. See [github-slugger](https://www.npmjs.com/package/github-slugger) for more information on how this is done. 87 | 88 | For example, the following html: 89 | 90 | ```html 91 |

A heading

92 | ``` 93 | 94 | becomes: 95 | 96 | ```html 97 |

A heading

98 | ``` 99 | 100 | This can be disabled with `{ prettyHeaderIds: false}`. 101 | 102 | ```js 103 | const tree = toHast(doc, { prettyHeaderIds: false }); 104 | ``` 105 | 106 | ## Unsupported Features 107 | 108 | Some features of Google Docs are not currently supported by this library. This list is not exhaustive. 109 | 110 | | Type | Supported | Bug | 111 | | ----------------------------------------------------------------------------- | --------- | --- | 112 | | Styles applied to embedded objects including borders, rotations, transparency | ❌ | | 113 | | `documentStyle` including pageSize, margins, etc | ❌ | | 114 | | `namedStyles` ( only added as class name on the appropriate tag ) | ❌ | | 115 | | Page numbers | ❌ | | 116 | | Page breaks | ❌ | | 117 | | Equations | ❌ | | 118 | | Columns | ❌ | | 119 | | Suggestions | ❌ | | 120 | | Bookmarks | ❌ | | 121 | 122 | > **Note:** This library does **not** intend to match the rendering by Google Docs. 123 | 124 | --- 125 | 126 | This is not an official Google product. 127 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Report a security issue 2 | 3 | To report a security issue, please use [https://g.co/vulnz](https://g.co/vulnz). We use 4 | [https://g.co/vulnz](https://g.co/vulnz) for our intake, and do coordination and disclosure here on 5 | GitHub (including using GitHub Security Advisory). The Google Security Team will 6 | respond within 5 working days of your report on [https://g.co/vulnz](https://g.co/vulnz). 7 | -------------------------------------------------------------------------------- /assets/docs.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --spacing: 0.5rem; 3 | } 4 | 5 | .tsd-panel > table { 6 | margin: var(--spacing) 0; 7 | border-collapse: collapse; 8 | } 9 | 10 | .tsd-panel td, 11 | .tsd-panel th { 12 | padding: var(--spacing); 13 | border: 1px solid var(--dark-code-background); 14 | text-align: center; 15 | } 16 | 17 | img[src="./assets/logo.jpg"] { 18 | width: 100%; 19 | } 20 | 21 | hr { 22 | margin: var(--spacing) 0; 23 | } 24 | 25 | @media (prefers-color-scheme: dark) { 26 | .tsd-panel td, 27 | .tsd-panel th { 28 | border-color: var(--light-code-background); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /assets/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleworkspace/google-docs-hast/777c68b22bf73d86f236a42f7b0e100c1f9e08f3/assets/logo.jpg -------------------------------------------------------------------------------- /e2e/__html__/complete.json.html: -------------------------------------------------------------------------------- 1 |

Named styles

2 |

Title

3 |

Subtitle

4 |

Heading 1

5 |

Heading 2

6 |

Heading 3

7 |

Heading 4

8 |
Heading 5
9 |
Heading 6
10 |

Normal Text

11 |

12 |

Text styles

13 |

Bold

14 |

Italics

15 |

Underline

16 |

Red

17 |

18 | Cyan 19 |

20 |

21 | BoldItalicsUnderlineRedCyan 30 |

31 |

Superscript

32 |

Subscript

33 |

Strikethrough

34 |

UPPERCASE

35 |

lowercase

36 |

Title Case

37 |

Larger font

38 |

Smaller font

39 |

40 | Courier New 43 |

44 |

45 | 48 |

49 |

White       space   patterns are         maintained.

50 |

Paragraph styles

51 |

Right Aligned Text

52 |

Center Aligned Text

53 |

54 | Justified Text: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed 55 | do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad 56 | minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 57 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit 58 | esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat 59 | non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 60 |

61 |

62 | Extra spacing 63 |

64 |

73 | Indented 74 |

75 |

89 | Bordered paragraph 90 |

91 |

Lists

92 |
    93 |
  • 94 | Unordered list bullet 95 |
      96 |
    • Nested unordered list bullet
    • 97 |
    98 |
  • 99 |
  • Another list bullet
  • 100 |
101 |
    102 |
  1. 103 | Ordered listed bullet 104 |
      105 |
    1. Nested ordered list bullet
    2. 106 |
    107 |
  2. 108 |
  3. Another list bullet
  4. 109 |
110 |

Tables

111 |

112 | 113 | 114 | 127 | 138 | 149 | 160 | 161 |
123 |

0

124 |

1

125 |

2

126 |
136 |

top

137 |
147 |

middle

148 |
158 |

bottom

159 |
162 |

163 | 164 | 165 | 184 | 185 |
174 |

175 | A table can contain other objects: 176 |

177 |

178 |

179 | 182 |

183 |
186 |

187 |

188 | 189 | 190 | 203 | 204 |
201 |

Custom borders and padding

202 |
205 |

206 |

207 | 208 | 209 | 220 | 231 | 232 | 233 | 244 | 255 | 256 |
218 |

A table row

219 |
229 |

Can have

230 |
242 |

Varying number of columns

243 |
253 |

254 |
257 |

258 |

259 | Inline Objects 260 |

261 |

262 | 265 |

266 |

267 | 270 |

271 |

272 |

273 | 276 |

277 |

Links

278 |

279 | Internal 280 |

281 |

282 |

example.com

283 |

footnote

284 |

Misc

285 |

286 |

287 |

😀

288 |

289 |

290 |

291 |

292 | 293 | 294 | 364 | 365 |
308 |

309 | const 325 | foo = 342 | "code block converted with addon"; 362 |

363 |
366 |

367 | const foo = “inline code”; 375 |

376 |

377 |

```js

378 |

const foo = “block”;

379 |

 let indented;

380 |

// some other code here

381 |

let bold;

382 |

(1 + 2)

383 |

```

384 |

385 |

386 | `const foo = “inline”;` and `here is more inline code` in a paragraph and but 387 | currently does not support `formatted inline code`. 388 |

389 |

390 |

391 | ``` 392 |

393 |

394 | foo = bar 395 |

396 |

397 | ``` 398 |

399 |

Columns

400 |

401 | 409 |

410 |

411 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 419 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 420 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 421 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 422 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat 423 | non proident, sunt in culpa qui officia deserunt mollit anim id est 424 | laborum. 426 |

427 |

428 | 436 |

437 |

438 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 446 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 447 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 448 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 449 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat 450 | non proident, sunt in culpa qui officia deserunt mollit anim id est 451 | laborum 453 |

454 |

455 | 463 |

464 |

465 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 473 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 474 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 475 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 476 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat 477 | non proident, sunt in culpa qui officia deserunt mollit anim id est 478 | laborum 480 |

481 |

482 | 490 |

491 |

492 |

End of columns

493 | -------------------------------------------------------------------------------- /e2e/__html__/complete.json_styles_false.html: -------------------------------------------------------------------------------- 1 |

Named styles

2 |

Title

3 |

Subtitle

4 |

Heading 1

5 |

Heading 2

6 |

Heading 3

7 |

Heading 4

8 |
Heading 5
9 |
Heading 6
10 |

Normal Text

11 |

12 |

Text styles

13 |

Bold

14 |

Italics

15 |

Underline

16 |

Red

17 |

Cyan

18 |

19 | BoldItalicsUnderlineRedCyan 22 |

23 |

Superscript

24 |

Subscript

25 |

Strikethrough

26 |

UPPERCASE

27 |

lowercase

28 |

Title Case

29 |

Larger font

30 |

Smaller font

31 |

Courier New

32 |

33 |

White       space   patterns are         maintained.

34 |

Paragraph styles

35 |

Right Aligned Text

36 |

Center Aligned Text

37 |

38 | Justified Text: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed 39 | do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad 40 | minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 41 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit 42 | esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat 43 | non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 44 |

45 |

Extra spacing

46 |

Indented

47 |

Bordered paragraph

48 |

Lists

49 |
    50 |
  • 51 | Unordered list bullet 52 |
      53 |
    • Nested unordered list bullet
    • 54 |
    55 |
  • 56 |
  • Another list bullet
  • 57 |
58 |
    59 |
  1. 60 | Ordered listed bullet 61 |
      62 |
    1. Nested ordered list bullet
    2. 63 |
    64 |
  2. 65 |
  3. Another list bullet
  4. 66 |
67 |

Tables

68 |

69 | 70 | 71 | 76 | 77 | 78 | 79 | 80 |
72 |

0

73 |

1

74 |

2

75 |

top

middle

bottom

81 |

82 | 83 | 84 | 95 | 96 |
85 |

86 | A table can contain other objects: 87 |

88 |

89 |

90 | 93 |

94 |
97 |

98 |

99 | 100 | 101 | 102 | 103 |

Custom borders and padding

104 |

105 |

106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 |

A table row

Can have

Varying number of columns

116 |

117 |

Inline Objects

118 |

119 | 122 |

123 |

124 | 127 |

128 |

129 |

130 | 133 |

134 |

Links

135 |

136 | Internal 137 |

138 |

139 |

example.com

140 |

footnote

141 |

Misc

142 |

143 |

144 |

😀

145 |

146 |

147 |

148 |

149 | 150 | 151 | 158 | 159 |
152 |

153 | const foo = "code block converted with addon"; 156 |

157 |
160 |

const foo = “inline code”;

161 |

162 |

```js

163 |

const foo = “block”;

164 |

 let indented;

165 |

// some other code here

166 |

let bold;

167 |

(1 + 2)

168 |

```

169 |

170 |

171 | `const foo = “inline”;` and `here is more inline code` in a paragraph and but 172 | currently does not support `formatted inline code`. 173 |

174 |

175 |

```

176 |

foo = bar

177 |

```

178 |

Columns

179 |

180 |

181 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor 182 | incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis 183 | nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 184 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore 185 | eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt 186 | in culpa qui officia deserunt mollit anim id est laborum. 187 |

188 |

189 |

190 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor 191 | incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis 192 | nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 193 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore 194 | eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt 195 | in culpa qui officia deserunt mollit anim id est laborum 196 |

197 |

198 |

199 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor 200 | incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis 201 | nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 202 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore 203 | eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt 204 | in culpa qui officia deserunt mollit anim id est laborum 205 |

206 |

207 |

208 |

End of columns

209 | -------------------------------------------------------------------------------- /e2e/__snapshots__/html.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1 2 | 3 | exports[`%name { name: 'complete.json' } > html 1`] = ` 4 |

Named styles

5 |

Title

6 |

Subtitle

7 |

Heading 1

8 |

Heading 2

9 |

Heading 3

10 |

Heading 4

11 |
Heading 5
12 |
Heading 6
13 |

Normal Text

14 |

15 |

Text styles

16 |

Bold

17 |

Italics

18 |

Underline

19 |

Red

20 |

21 | Cyan 22 |

23 |

24 | BoldItalicsUnderlineRedCyan 33 |

34 |

Superscript

35 |

Subscript

36 |

Strikethrough

37 |

UPPERCASE

38 |

lowercase

39 |

Title Case

40 |

Larger font

41 |

Smaller font

42 |

43 | Courier New 46 |

47 |

48 | 51 |

52 |

White       space   patterns are         maintained.

53 |

Paragraph styles

54 |

Right Aligned Text

55 |

Center Aligned Text

56 |

57 | Justified Text: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed 58 | do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad 59 | minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 60 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit 61 | esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat 62 | non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 63 |

64 |

65 | Extra spacing 66 |

67 |

76 | Indented 77 |

78 |

92 | Bordered paragraph 93 |

94 |

Lists

95 |
    96 |
  • 97 | Unordered list bullet 98 |
      99 |
    • Nested unordered list bullet
    • 100 |
    101 |
  • 102 |
  • Another list bullet
  • 103 |
104 |
    105 |
  1. 106 | Ordered listed bullet 107 |
      108 |
    1. Nested ordered list bullet
    2. 109 |
    110 |
  2. 111 |
  3. Another list bullet
  4. 112 |
113 |

Tables

114 |

115 | 116 | 117 | 130 | 141 | 152 | 163 | 164 |
126 |

0

127 |

1

128 |

2

129 |
139 |

top

140 |
150 |

middle

151 |
161 |

bottom

162 |
165 |

166 | 167 | 168 | 187 | 188 |
177 |

178 | A table can contain other objects: 179 |

180 |

181 |

182 | 185 |

186 |
189 |

190 |

191 | 192 | 193 | 206 | 207 |
204 |

Custom borders and padding

205 |
208 |

209 |

210 | 211 | 212 | 223 | 234 | 235 | 236 | 247 | 258 | 259 |
221 |

A table row

222 |
232 |

Can have

233 |
245 |

Varying number of columns

246 |
256 |

257 |
260 |

261 |

262 | Inline Objects 263 |

264 |

265 | 268 |

269 |

270 | 273 |

274 |

275 |

276 | 279 |

280 |

Links

281 |

282 | Internal 283 |

284 |

285 |

example.com

286 |

footnote

287 |

Misc

288 |

289 |

290 |

😀

291 |

292 |

293 |

294 |

295 | 296 | 297 | 367 | 368 |
311 |

312 | const 328 | foo = 345 | "code block converted with addon"; 365 |

366 |
369 |

370 | const foo = “inline code”; 378 |

379 |

380 |

\`\`\`js

381 |

const foo = “block”;

382 |

 let indented;

383 |

// some other code here

384 |

let bold;

385 |

(1 + 2)

386 |

\`\`\`

387 |

388 |

389 | \`const foo = “inline”;\` and \`here is more inline code\` in a paragraph and but 390 | currently does not support \`formatted inline code\`. 391 |

392 |

393 |

394 | \`\`\` 395 |

396 |

397 | foo = bar 398 |

399 |

400 | \`\`\` 401 |

402 |

Columns

403 |

404 | 412 |

413 |

414 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 422 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 423 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 424 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 425 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat 426 | non proident, sunt in culpa qui officia deserunt mollit anim id est 427 | laborum. 429 |

430 |

431 | 439 |

440 |

441 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 449 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 450 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 451 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 452 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat 453 | non proident, sunt in culpa qui officia deserunt mollit anim id est 454 | laborum 456 |

457 |

458 | 466 |

467 |

468 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 476 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 477 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 478 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 479 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat 480 | non proident, sunt in culpa qui officia deserunt mollit anim id est 481 | laborum 483 |

484 |

485 | 493 |

494 |

495 |

End of columns

496 | 497 | `; 498 | 499 | exports[`%name { name: 'complete.json' } > html without styles 1`] = ` 500 |

Named styles

501 |

Title

502 |

Subtitle

503 |

Heading 1

504 |

Heading 2

505 |

Heading 3

506 |

Heading 4

507 |
Heading 5
508 |
Heading 6
509 |

Normal Text

510 |

511 |

Text styles

512 |

Bold

513 |

Italics

514 |

Underline

515 |

Red

516 |

Cyan

517 |

518 | BoldItalicsUnderlineRedCyan 521 |

522 |

Superscript

523 |

Subscript

524 |

Strikethrough

525 |

UPPERCASE

526 |

lowercase

527 |

Title Case

528 |

Larger font

529 |

Smaller font

530 |

Courier New

531 |

532 |

White       space   patterns are         maintained.

533 |

Paragraph styles

534 |

Right Aligned Text

535 |

Center Aligned Text

536 |

537 | Justified Text: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed 538 | do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad 539 | minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 540 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit 541 | esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat 542 | non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 543 |

544 |

Extra spacing

545 |

Indented

546 |

Bordered paragraph

547 |

Lists

548 |
    549 |
  • 550 | Unordered list bullet 551 |
      552 |
    • Nested unordered list bullet
    • 553 |
    554 |
  • 555 |
  • Another list bullet
  • 556 |
557 |
    558 |
  1. 559 | Ordered listed bullet 560 |
      561 |
    1. Nested ordered list bullet
    2. 562 |
    563 |
  2. 564 |
  3. Another list bullet
  4. 565 |
566 |

Tables

567 |

568 | 569 | 570 | 575 | 576 | 577 | 578 | 579 |
571 |

0

572 |

1

573 |

2

574 |

top

middle

bottom

580 |

581 | 582 | 583 | 594 | 595 |
584 |

585 | A table can contain other objects: 586 |

587 |

588 |

589 | 592 |

593 |
596 |

597 |

598 | 599 | 600 | 601 | 602 |

Custom borders and padding

603 |

604 |

605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 |

A table row

Can have

Varying number of columns

615 |

616 |

Inline Objects

617 |

618 | 621 |

622 |

623 | 626 |

627 |

628 |

629 | 632 |

633 |

Links

634 |

635 | Internal 636 |

637 |

638 |

example.com

639 |

footnote

640 |

Misc

641 |

642 |

643 |

😀

644 |

645 |

646 |

647 |

648 | 649 | 650 | 657 | 658 |
651 |

652 | const foo = "code block converted with addon"; 655 |

656 |
659 |

const foo = “inline code”;

660 |

661 |

\`\`\`js

662 |

const foo = “block”;

663 |

 let indented;

664 |

// some other code here

665 |

let bold;

666 |

(1 + 2)

667 |

\`\`\`

668 |

669 |

670 | \`const foo = “inline”;\` and \`here is more inline code\` in a paragraph and but 671 | currently does not support \`formatted inline code\`. 672 |

673 |

674 |

\`\`\`

675 |

foo = bar

676 |

\`\`\`

677 |

Columns

678 |

679 |

680 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor 681 | incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis 682 | nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 683 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore 684 | eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt 685 | in culpa qui officia deserunt mollit anim id est laborum. 686 |

687 |

688 |

689 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor 690 | incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis 691 | nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 692 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore 693 | eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt 694 | in culpa qui officia deserunt mollit anim id est laborum 695 |

696 |

697 |

698 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor 699 | incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis 700 | nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 701 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore 702 | eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt 703 | in culpa qui officia deserunt mollit anim id est laborum 704 |

705 |

706 |

707 |

End of columns

708 | 709 | `; 710 | -------------------------------------------------------------------------------- /e2e/cases.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import fs from "fs"; 18 | import path from "path"; 19 | 20 | export const FIXTURES_DIR = path.join(__dirname, "__fixtures__"); 21 | export const CASES = fs.readdirSync(FIXTURES_DIR).map((f) => ({ 22 | name: path.basename(f), 23 | })); 24 | -------------------------------------------------------------------------------- /e2e/hast.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import fs from "fs"; 18 | import path from "path"; 19 | 20 | import { describe, expect, test } from "vitest"; 21 | 22 | import { toHast } from "../src"; 23 | import { FIXTURES_DIR, CASES } from "./cases"; 24 | 25 | expect.addSnapshotSerializer({ 26 | serialize(val) { 27 | return JSON.stringify(val, null, 2); 28 | }, 29 | test(val) { 30 | return typeof val === "object"; 31 | }, 32 | }); 33 | 34 | describe.each(CASES)("%name", ({ name }) => { 35 | const doc = JSON.parse( 36 | fs.readFileSync(path.join(FIXTURES_DIR, name), "utf8") 37 | ); 38 | 39 | test("hast", () => { 40 | expect(toHast(doc)).toMatchSnapshot(); 41 | }); 42 | 43 | test("hast without styles", () => { 44 | expect(toHast(doc, { styles: false })).toMatchSnapshot(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /e2e/html.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import fs from "fs"; 18 | import path from "path"; 19 | 20 | import prettier from "prettier"; 21 | import rehypeStringify from "rehype-stringify"; 22 | import { unified } from "unified"; 23 | import { describe, expect, test } from "vitest"; 24 | 25 | import { toHast } from "../src"; 26 | import { CASES, FIXTURES_DIR } from "./cases"; 27 | 28 | import type { Root } from "hast"; 29 | 30 | expect.addSnapshotSerializer({ 31 | serialize(val) { 32 | return val; 33 | }, 34 | test(val) { 35 | return typeof val === "string"; 36 | }, 37 | }); 38 | 39 | describe.each(CASES)("%name", ({ name }) => { 40 | const doc = JSON.parse( 41 | fs.readFileSync(path.join(FIXTURES_DIR, name), "utf8") 42 | ); 43 | 44 | test("html", () => { 45 | const output = prettier.format(html(toHast(doc)), { parser: "html" }); 46 | fs.mkdirSync(path.join(__dirname, "__html__"), { recursive: true }); 47 | fs.writeFileSync(path.join(__dirname, "__html__", `${name}.html`), output); 48 | expect(output).toMatchSnapshot(); 49 | }); 50 | 51 | test("html without styles", () => { 52 | const output = prettier.format(html(toHast(doc, { styles: false })), { 53 | parser: "html", 54 | }); 55 | fs.mkdirSync(path.join(__dirname, "__html__"), { recursive: true }); 56 | fs.writeFileSync( 57 | path.join(__dirname, "__html__", `${name}_styles_false.html`), 58 | output 59 | ); 60 | expect(output).toMatchSnapshot(); 61 | }); 62 | }); 63 | 64 | const html = (root: Root): string => { 65 | return unified() 66 | .use(rehypeStringify, { collapseEmptyAttributes: true }) 67 | .stringify(root); 68 | }; 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@googleworkspace/google-docs-hast", 3 | "version": "1.0.5", 4 | "type": "module", 5 | "description": "Convert a Google Doc JSON representation to an HTML abstract syntax tree.", 6 | "main": "dist/index.js", 7 | "types": "dist/src/index.d.ts", 8 | "homepage": "https://github.com/googleworkspace/google-docs-hast", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/googleworkspace/google-docs-hast.git" 12 | }, 13 | "scripts": { 14 | "build": "tsc --emitDeclarationOnly && esbuild src/index.ts --format=esm --outfile=dist/index.js --sourcemap --bundle", 15 | "docs": "typedoc --options typedoc.cjs && cp assets/logo.jpg docs/assets/logo.jpg", 16 | "format": "eslint . --fix", 17 | "lint": "eslint .", 18 | "prepare": "npm run build", 19 | "test": "vitest" 20 | }, 21 | "files": [ 22 | "src", 23 | "dist" 24 | ], 25 | "keywords": [ 26 | "google", 27 | "docs", 28 | "hast", 29 | "html", 30 | "unified" 31 | ], 32 | "author": { 33 | "name": "Justin Poehnelt", 34 | "email": "jpoehnelt@google.com" 35 | }, 36 | "license": "Apache-2.0", 37 | "dependencies": { 38 | "github-slugger": "^1.4.0", 39 | "hast-util-to-string": "^2.0.0", 40 | "hastscript": "^7.0.2" 41 | }, 42 | "devDependencies": { 43 | "@googleapis/docs": "^0.4.2", 44 | "@types/node": "^17.0.42", 45 | "@types/prettier": "^2.6.3", 46 | "@typescript-eslint/eslint-plugin": "^5.40.0", 47 | "@typescript-eslint/parser": "^5.40.0", 48 | "c8": "^7.12.0", 49 | "csstype": "^3.1.1", 50 | "esbuild": "^0.14.36", 51 | "eslint": "^8.25.0", 52 | "eslint-config-prettier": "^8.5.0", 53 | "eslint-plugin-import": "^2.26.0", 54 | "eslint-plugin-jsdoc": "^39.3.13", 55 | "eslint-plugin-prettier": "^4.2.1", 56 | "googleapis": "^103.0.0", 57 | "hast-util-to-mdast": "^8.3.1", 58 | "prettier": "^2.6.2", 59 | "rehype-stringify": "^9.0.3", 60 | "remark-stringify": "^10.0.2", 61 | "typedoc": "^0.23.17", 62 | "typescript": "^4.8.4", 63 | "unified": "^10.1.2", 64 | "vitest": "^0.14.2" 65 | }, 66 | "publishConfig": { 67 | "access": "public", 68 | "registry": "https://wombat-dressing-room.appspot.com" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/hast/common/paragraphStyle.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { borders, textAlign, serializeStyle } from "./style"; 18 | 19 | import type { docs_v1 } from "@googleapis/docs"; 20 | import type { Properties } from "csstype"; 21 | 22 | export const paragraphStyleToAttributes = ({ 23 | alignment, 24 | borderBetween, 25 | borderBottom, 26 | borderLeft, 27 | borderRight, 28 | borderTop, 29 | direction, 30 | indentEnd, 31 | indentFirstLine, 32 | indentStart, 33 | lineSpacing, 34 | namedStyleType, 35 | spaceAbove, 36 | spaceBelow, 37 | headingId, 38 | }: docs_v1.Schema$ParagraphStyle): { 39 | style?: string; 40 | class?: string; 41 | id?: string; 42 | } => { 43 | const style: Properties = {}; 44 | 45 | if (alignment) { 46 | const value = textAlign(alignment); 47 | if (value !== "start") { 48 | style.textAlign = textAlign(alignment); 49 | } 50 | } 51 | 52 | Object.assign( 53 | style, 54 | borders({ borderBottom, borderLeft, borderRight, borderTop, borderBetween }) 55 | ); 56 | 57 | if (direction === "RIGHT_TO_LEFT") { 58 | style.direction = "rtl"; 59 | } 60 | 61 | // TODO should indents be padding or margin or something else? 62 | // TODO how does this interact with border.padding? 63 | if (indentStart?.magnitude != undefined) { 64 | style.paddingLeft = `${indentStart.magnitude}${indentStart.unit}`; 65 | } 66 | 67 | if (indentEnd?.magnitude != undefined) { 68 | style.paddingRight = `${indentEnd.magnitude}${indentEnd.unit}`; 69 | } 70 | 71 | if (indentFirstLine?.magnitude != undefined) { 72 | style.textIndent = `${indentFirstLine.magnitude}${indentFirstLine.unit}`; 73 | } 74 | 75 | if (lineSpacing && lineSpacing !== 100) { 76 | style.lineHeight = `${lineSpacing}%`; 77 | } 78 | 79 | if (spaceAbove?.magnitude != undefined) { 80 | style.marginTop = `${spaceAbove.magnitude}${spaceAbove.unit}`; 81 | } 82 | 83 | if (spaceBelow?.magnitude != undefined) { 84 | style.marginBottom = `${spaceBelow.magnitude}${spaceBelow.unit}`; 85 | } 86 | 87 | const attributes: ReturnType = {}; 88 | 89 | if (Object.keys(style).length) { 90 | attributes.style = serializeStyle(style); 91 | } 92 | 93 | if (namedStyleType) { 94 | attributes.class = namedStyleType.replace(/_/g, "-").toLowerCase(); 95 | } 96 | 97 | if (headingId) { 98 | attributes.id = headingId; 99 | } 100 | 101 | return attributes; 102 | }; 103 | -------------------------------------------------------------------------------- /src/hast/common/style.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { describe, expect, test } from "vitest"; 18 | 19 | import { borderToCss } from "./style"; 20 | 21 | import type { docs_v1 } from "@googleapis/docs"; 22 | 23 | describe("common/style", () => { 24 | test("borderToCss", () => { 25 | const border: docs_v1.Schema$ParagraphBorder = { 26 | color: { 27 | color: { 28 | rgbColor: { 29 | blue: 0.5, 30 | green: 0.5, 31 | red: 0.5, 32 | }, 33 | }, 34 | }, 35 | dashStyle: "SOLID", 36 | width: { 37 | magnitude: 1, 38 | unit: "PT", 39 | }, 40 | }; 41 | 42 | expect(borderToCss(border)).toMatchInlineSnapshot( 43 | '"1PT rgb(127.5, 127.5, 127.5) solid"' 44 | ); 45 | expect(borderToCss({ ...border, width: undefined })).toMatchInlineSnapshot( 46 | '""' 47 | ); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/hast/common/style.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import type { docs_v1 } from "@googleapis/docs"; 17 | import type { Properties, Property } from "csstype"; 18 | 19 | export const textAlign = ( 20 | alignment: docs_v1.Schema$ParagraphStyle["alignment"] 21 | ): Property.TextAlign => { 22 | const mapping: { 23 | [key: docs_v1.Schema$ParagraphStyle["alignment"]]: Property.TextAlign; 24 | } = { 25 | ALIGNMENT_UNSPECIFIED: "start", 26 | START: "start", 27 | CENTER: "center", 28 | END: "end", 29 | JUSTIFIED: "justify", 30 | }; 31 | 32 | return mapping[alignment]; 33 | }; 34 | 35 | export const borderToCss = ({ 36 | color, 37 | width, 38 | dashStyle, 39 | }: docs_v1.Schema$ParagraphBorder): string => { 40 | if (width?.magnitude === undefined) return ""; 41 | 42 | const borderStyle = { DOT: "dotted", DASH: "dashed", SOLID: "solid" }[ 43 | dashStyle 44 | ]; 45 | return `${width.magnitude}${width.unit} ${rgbColor(color)} ${borderStyle}`; 46 | }; 47 | 48 | type Border = 49 | | docs_v1.Schema$ParagraphBorder 50 | | docs_v1.Schema$TableCellBorder 51 | | docs_v1.Schema$EmbeddedObjectBorder; 52 | 53 | export const borders = ({ 54 | borderLeft, 55 | borderRight, 56 | borderTop, 57 | borderBottom, 58 | borderBetween, 59 | }: { 60 | borderLeft?: Border; 61 | borderRight?: Border; 62 | borderTop?: Border; 63 | borderBottom?: Border; 64 | borderBetween?: docs_v1.Schema$ParagraphBorder; 65 | }): Properties => { 66 | const style: Properties = {}; 67 | 68 | if (borderLeft?.width?.magnitude) { 69 | style.borderLeft = borderToCss(borderLeft); 70 | } 71 | 72 | if (borderRight?.width?.magnitude) { 73 | style.borderRight = borderToCss(borderRight); 74 | } 75 | 76 | if (borderTop?.width?.magnitude) { 77 | style.borderTop = borderToCss(borderTop); 78 | } 79 | 80 | if (borderBottom?.width?.magnitude) { 81 | style.borderBottom = borderToCss(borderBottom); 82 | } 83 | 84 | if (borderBetween?.width?.magnitude) { 85 | style.borderTop = borderToCss(borderBetween); 86 | } 87 | 88 | if (borderBetween?.padding?.magnitude) { 89 | style.paddingTop = `${borderBetween.padding.magnitude}${borderBetween.padding.unit}`; 90 | style.paddingBottom = `${borderBetween.padding.magnitude}${borderBetween.padding.unit}`; 91 | } 92 | 93 | if (isParagraphBorder(borderBottom) && borderBottom?.padding?.magnitude) { 94 | style.paddingBottom = `${borderBottom.padding.magnitude}${borderBottom.padding.unit}`; 95 | } 96 | 97 | if (isParagraphBorder(borderTop) && borderTop?.padding?.magnitude) { 98 | style.paddingBottom = `${borderTop.padding.magnitude}${borderTop.padding.unit}`; 99 | } 100 | 101 | if (isParagraphBorder(borderLeft) && borderLeft?.padding?.magnitude) { 102 | style.paddingLeft = `${borderLeft.padding.magnitude}${borderLeft.padding.unit}`; 103 | } 104 | 105 | if (isParagraphBorder(borderRight) && borderRight?.padding?.magnitude) { 106 | style.paddingRight = `${borderRight.padding.magnitude}${borderRight.padding.unit}`; 107 | } 108 | 109 | return style; 110 | }; 111 | 112 | const isParagraphBorder = ( 113 | border: Border 114 | ): border is docs_v1.Schema$ParagraphBorder => { 115 | return border && "padding" in border; 116 | }; 117 | 118 | export const rgbColor = ({ 119 | color: { rgbColor: { red = 0, green = 0, blue = 0 } = {} } = {}, 120 | }: docs_v1.Schema$OptionalColor): string => { 121 | return `rgb(${red * 255}, ${green * 255}, ${blue * 255})`; 122 | }; 123 | 124 | export const namedStyleTypeToTag = (namedStyleType: string): string => { 125 | return ( 126 | { 127 | NORMAL_TEXT: "p", 128 | TITLE: "h1", 129 | SUBTITLE: "p", 130 | HEADING_1: "h1", 131 | HEADING_2: "h2", 132 | HEADING_3: "h3", 133 | HEADING_4: "h4", 134 | HEADING_5: "h5", 135 | HEADING_6: "h6", 136 | }[namedStyleType] ?? "p" 137 | ); 138 | }; 139 | 140 | export const camelToKebabCase = (str: string): string => 141 | str.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`); 142 | 143 | export const serializeStyle = (style: Properties): string => 144 | Object.entries(style) 145 | .map(([key, value]) => `${camelToKebabCase(key)}: ${value}`) 146 | .join("; "); 147 | -------------------------------------------------------------------------------- /src/hast/common/wrapStyle.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { describe, expect, test } from "vitest"; 18 | 19 | import { wrapStyle } from "./wrapStyle"; 20 | 21 | describe("textStyle", () => { 22 | test("bold", () => { 23 | expect( 24 | wrapStyle( 25 | { 26 | type: "text", 27 | value: "Hello", 28 | }, 29 | { bold: true } 30 | ) 31 | ).toMatchInlineSnapshot(` 32 | { 33 | "children": [ 34 | { 35 | "type": "text", 36 | "value": "Hello", 37 | }, 38 | ], 39 | "properties": {}, 40 | "tagName": "strong", 41 | "type": "element", 42 | } 43 | `); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/hast/common/wrapStyle.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { h } from "hastscript"; 18 | 19 | import { rgbColor, serializeStyle } from "../common/style"; 20 | 21 | import type { docs_v1 } from "@googleapis/docs"; 22 | import type { Element, Text } from "hast"; 23 | 24 | /** 25 | * Wraps the underlying element in HTML tags such as `` and `` and applies 26 | * CSS styles to the inner element for styles not capture by HTML tags. 27 | * 28 | * @internal 29 | */ 30 | export const wrapStyle = ( 31 | el: Element | Text, 32 | { 33 | backgroundColor, 34 | baselineOffset, 35 | bold, 36 | fontSize, 37 | foregroundColor, 38 | italic, 39 | strikethrough, 40 | underline, 41 | weightedFontFamily: { weight, fontFamily } = {}, 42 | }: docs_v1.Schema$TextStyle = {} 43 | ): Element | Text => { 44 | const style: { [key: string]: string } = {}; 45 | 46 | if (backgroundColor) { 47 | style.backgroundColor = rgbColor(backgroundColor); 48 | } 49 | 50 | if (fontSize) { 51 | style.fontSize = `${fontSize.magnitude}${fontSize.unit}`; 52 | } 53 | 54 | if (fontFamily) { 55 | style.fontFamily = fontFamily; 56 | } 57 | 58 | if (weight) { 59 | style.fontWeight = String(weight); 60 | } 61 | 62 | if (foregroundColor) { 63 | style.color = rgbColor(foregroundColor); 64 | } 65 | 66 | if (Object.keys(style).length) { 67 | // upgrade to span if text node 68 | if (isText(el)) { 69 | el = h("span", { style: serializeStyle(style) }, el.value); 70 | } else { 71 | el.properties.style = serializeStyle(style); 72 | } 73 | } 74 | 75 | if (baselineOffset === "SUPERSCRIPT") { 76 | el = h("sup", [el]); 77 | } 78 | 79 | if (baselineOffset === "SUBSCRIPT") { 80 | el = h("sub", [el]); 81 | } 82 | 83 | if (bold) { 84 | el = h("strong", [el]); 85 | } 86 | 87 | if (italic) { 88 | el = h("i", [el]); 89 | } 90 | 91 | if (strikethrough) { 92 | el = h("s", [el]); 93 | } 94 | 95 | if (underline) { 96 | el = h("u", [el]); 97 | } 98 | 99 | return el; 100 | }; 101 | 102 | export const isText = (o: Element | Text): o is Text => { 103 | return o.type === "text"; 104 | }; 105 | -------------------------------------------------------------------------------- /src/hast/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { h } from "hastscript"; 18 | 19 | import { 20 | listElement, 21 | isListItem, 22 | listItemLevel, 23 | listItemBulletId, 24 | } from "./lists"; 25 | import { paragraphToElement } from "./paragraph"; 26 | import { replaceHeaderIdsWithSlug } from "./postProcessing/prettyHeaderIds"; 27 | import { removeStyles } from "./postProcessing/removeStyles"; 28 | import { tableToElement } from "./table"; 29 | 30 | import type { docs_v1 } from "@googleapis/docs"; 31 | import type { Element, Root } from "hast"; 32 | 33 | export interface HastOptions { 34 | /** 35 | * Header IDs are in the form `id="h.wn8l66qm9m7y"` when exported from the 36 | * Google Docs API. By default, header tag IDs are updated to match their text 37 | * content. See [github-slugger](https://www.npmjs.com/package/github-slugger) 38 | * for more information on how this is done. 39 | * 40 | * The default behavior can be disabled by setting this option to `false`. 41 | */ 42 | prettyHeaderIds?: boolean; 43 | /** 44 | * Styles are added to elements by default. This can be disabled by setting 45 | * this option to `false`. 46 | */ 47 | styles?: boolean; 48 | } 49 | 50 | export const DEFAULT_OPTIONS = { prettyHeaderIds: true, styles: true }; 51 | 52 | export interface Context { 53 | options: HastOptions; 54 | doc: docs_v1.Schema$Document; 55 | } 56 | 57 | /** 58 | * Generate an HTML AST from a Google Docs document in JSON form. 59 | * 60 | * @param doc JSON representation of a Google Docs document. 61 | * @param options Options for the transformation. 62 | * @returns The HTML abstract syntax tree. 63 | * @see {@link https://developers.google.com/docs/api/reference/rest/v1/documents#Document | Google Docs API}. 64 | * @see {@link https://github.com/syntax-tree/hast | HTML abstract syntax tree (HAST)} 65 | */ 66 | export const toHast = ( 67 | doc: docs_v1.Schema$Document, 68 | options: HastOptions = {} 69 | ): Root => { 70 | options = { ...DEFAULT_OPTIONS, ...options }; 71 | 72 | // TODO headers, footers, footnotes, etc 73 | // @see https://developers.google.com/docs/api/reference/rest/v1/documents#Document 74 | const { body } = doc; 75 | 76 | let tree = h(null, transform(body.content, { doc, options })); 77 | 78 | if (options.prettyHeaderIds) { 79 | tree = replaceHeaderIdsWithSlug(tree); 80 | } 81 | 82 | if (options.styles === false) { 83 | tree = removeStyles(tree); 84 | } 85 | 86 | return tree; 87 | }; 88 | 89 | /** 90 | * @deprecated Use {@link toHast} instead. 91 | */ 92 | export const hast = toHast; 93 | 94 | export const transform = ( 95 | content: docs_v1.Schema$StructuralElement[], 96 | context: Context 97 | ): Element[] => { 98 | return content?.reduce((acc, ...args) => { 99 | const el = structuralElementToElement(acc, ...args, context); 100 | 101 | // could be undefined because the element was added as a child of another 102 | // element. e.g. `li` inside a `ul` 103 | if (el) { 104 | acc.push(el); 105 | } 106 | 107 | return acc; 108 | }, []); 109 | }; 110 | 111 | const structuralElementToElement = ( 112 | acc: Element[], 113 | el: docs_v1.Schema$StructuralElement, 114 | index: number, 115 | elements: docs_v1.Schema$StructuralElement[], 116 | context: Context 117 | ): Element => { 118 | // Union field content can be only one of the following: 119 | // @see https://developers.google.com/docs/api/reference/rest/v1/documents#StructuralElement 120 | const { paragraph, table } = el; 121 | 122 | if (paragraph) { 123 | const last = elements[index - 1]; 124 | const parent = acc.at(-1); //[acc.length - 1]; 125 | 126 | const renderedElement: Element = paragraphToElement(paragraph, context); 127 | 128 | if (isListItem(el)) { 129 | const elListItemLevel = listItemLevel(el); 130 | if ( 131 | isListItem(last) && 132 | (listItemBulletId(el) == listItemBulletId(last) || elListItemLevel > 0) 133 | ) { 134 | const lastListItemLevel = listItemLevel(last); 135 | let level: Element = parent; 136 | // nested list item 137 | if (elListItemLevel > lastListItemLevel) { 138 | // traverse from top level `parent` to `el` level - 1 deep 139 | level = traverseToListLevel(parent, elListItemLevel - 1); 140 | const list = listElement(el, context); 141 | list.children.push(renderedElement); 142 | getElementLastChild(level).children.push(list); 143 | return null; 144 | } 145 | // item on existing list 146 | else { 147 | // traverse from top level `parent` to `el` level - 1 deep 148 | level = traverseToListLevel(parent, elListItemLevel); 149 | level.children.push(renderedElement); 150 | return null; 151 | } 152 | } 153 | // new list 154 | else { 155 | const list = listElement(el, context); 156 | list.children.push(renderedElement); 157 | return list; 158 | } 159 | } 160 | 161 | return renderedElement; 162 | } 163 | 164 | if (table) { 165 | return tableToElement(table, context); 166 | } 167 | 168 | console.warn( 169 | `Unsupported element: ${Object.keys(el) 170 | .filter((k) => !k.match(/.*Index$/)) 171 | .pop()}` 172 | ); 173 | }; 174 | const isElement = (e: Element): e is Element => e.type === "element"; 175 | const getElementLastChild = (e: Element): Element => { 176 | return e.children.filter(isElement).at(-1); 177 | }; 178 | 179 | // traverse from top level of list element `e` to `l` level filling the hole in between 180 | const traverseToListLevel = (e: Element, l: number): Element => { 181 | let e_new_level = e; 182 | // since list contains 2 level itself we need to double our steps... 183 | for (let i = 0; i < 2 * l; i++) { 184 | const e_try_level = getElementLastChild(e_new_level); 185 | if (e_try_level && e_try_level.type === "element") { 186 | // ok to dive deeper 187 | e_new_level = e_try_level; 188 | } else { 189 | // build empty levels to connect a hole between `last` and `el` 190 | let e_empty: Element; 191 | e_new_level.tagName == "li" 192 | ? (e_empty = h("ul", h("li"))) 193 | : (e_empty = h("li")); 194 | e_new_level.children.push(e_empty); 195 | e_new_level = e_empty; 196 | } 197 | } 198 | return e_new_level; 199 | }; 200 | -------------------------------------------------------------------------------- /src/hast/lists.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { h } from "hastscript"; 18 | 19 | import type { Context } from "."; 20 | import type { docs_v1 } from "@googleapis/docs"; 21 | import type { Element } from "hast"; 22 | 23 | export const isListItem = (el: docs_v1.Schema$StructuralElement): boolean => { 24 | return Boolean(el.paragraph && el.paragraph.bullet); 25 | }; 26 | 27 | export const listItemLevel = (el: docs_v1.Schema$StructuralElement): number => { 28 | return el.paragraph.bullet.nestingLevel ?? 0; 29 | }; 30 | export const listItemBulletId = ( 31 | el: docs_v1.Schema$StructuralElement 32 | ): string => { 33 | return el.paragraph.bullet.listId; 34 | }; 35 | 36 | export const listElement = ( 37 | el: docs_v1.Schema$StructuralElement, 38 | { doc }: Context 39 | ): Element => { 40 | const { listId } = el.paragraph.bullet; 41 | let { nestingLevel } = el.paragraph.bullet; 42 | nestingLevel = nestingLevel ?? 0; 43 | const { glyphType, startNumber } = 44 | doc.lists[listId].listProperties.nestingLevels[nestingLevel]; 45 | 46 | const attributes = { 47 | class: `nesting-level-${nestingLevel + 1}`, 48 | }; 49 | 50 | if ([undefined, "GLYPH_TYPE_UNSPECIFIED", "NONE"].includes(glyphType)) { 51 | return h("ul", attributes); 52 | } 53 | 54 | const listStyleType = { 55 | DECIMAL: "decimal", 56 | ZERO_DECIMAL: "decimal-leading-zero", 57 | UPPER_ALPHA: "upper-alpha", 58 | ALPHA: "lower-alpha", 59 | UPPER_ROMAN: "upper-roman", 60 | ROMAN: "lower-roman", 61 | }; 62 | 63 | const style = listStyleType[glyphType]; 64 | 65 | return h("ol", { 66 | ...attributes, 67 | ...(startNumber !== 1 ? { start: String(startNumber) } : {}), 68 | ...(style ? { "list-style-type": style } : {}), 69 | }); 70 | }; 71 | -------------------------------------------------------------------------------- /src/hast/paragraph/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { h } from "hastscript"; 18 | 19 | import { paragraphStyleToAttributes } from "../common/paragraphStyle"; 20 | import { namedStyleTypeToTag } from "../common/style"; 21 | import { transformInlineObject } from "./inlineObject"; 22 | import { transformPerson } from "./person"; 23 | import { transformRichLink } from "./richLink"; 24 | import { transformTextRun } from "./textRun"; 25 | 26 | import type { Context } from ".."; 27 | import type { docs_v1 } from "@googleapis/docs"; 28 | import type { Element, Text } from "hast"; 29 | 30 | export const paragraphToElement = ( 31 | paragraph: docs_v1.Schema$Paragraph, 32 | { doc }: Context 33 | ): Element => { 34 | // @see https://developers.google.com/docs/api/reference/rest/v1/documents#Paragraph 35 | const { elements, paragraphStyle, bullet } = paragraph; 36 | 37 | // TODO apply styles to bullets 38 | if (bullet) { 39 | return h( 40 | "li", 41 | elements.map((el) => paragraphElementToElement(el, doc)) 42 | ); 43 | } 44 | return h( 45 | namedStyleTypeToTag(paragraphStyle.namedStyleType), 46 | paragraphStyleToAttributes(paragraphStyle), 47 | elements 48 | .map((el) => paragraphElementToElement(el, doc)) 49 | .filter((el) => el != undefined) 50 | ); 51 | }; 52 | 53 | const paragraphElementToElement = ( 54 | el: docs_v1.Schema$ParagraphElement, 55 | doc: docs_v1.Schema$Document 56 | ): Element | Text => { 57 | // @see https://developers.google.com/docs/api/reference/rest/v1/documents#ParagraphElement 58 | // TODO support other types of paragraph elements 59 | const { inlineObjectElement, person, richLink, textRun } = el; 60 | 61 | if (inlineObjectElement) { 62 | return transformInlineObject(inlineObjectElement, doc); 63 | } 64 | 65 | if (person) { 66 | return transformPerson(person); 67 | } 68 | 69 | if (richLink) { 70 | return transformRichLink(richLink); 71 | } 72 | 73 | if (textRun) { 74 | return transformTextRun(textRun); 75 | } 76 | 77 | console.warn( 78 | `Unsupported element: paragraph.${Object.keys(el) 79 | .filter((k) => !k.match(/.*Index$/)) 80 | .pop()}` 81 | ); 82 | }; 83 | -------------------------------------------------------------------------------- /src/hast/paragraph/inlineObject.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { h } from "hastscript"; 18 | 19 | import { wrapStyle } from "../common/wrapStyle"; 20 | 21 | import type { docs_v1 } from "@googleapis/docs"; 22 | import type { Element, Text } from "hast"; 23 | 24 | export const transformInlineObject = ( 25 | { inlineObjectId, textStyle }: docs_v1.Schema$InlineObjectElement, 26 | doc: docs_v1.Schema$Document 27 | ): Element | Text => { 28 | const { imageProperties } = 29 | doc.inlineObjects[inlineObjectId].inlineObjectProperties.embeddedObject; 30 | 31 | if (imageProperties) { 32 | const el = h("img", { src: imageProperties.contentUri }); 33 | return wrapStyle(el, textStyle); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/hast/paragraph/person.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { describe, expect, test } from "vitest"; 18 | 19 | import { transformPerson } from "./person"; 20 | 21 | import type { docs_v1 } from "@googleapis/docs"; 22 | 23 | describe("person", () => { 24 | const person: docs_v1.Schema$ParagraphElement["person"] = { 25 | personId: "personId", 26 | personProperties: { 27 | email: "john@example.com", 28 | name: "John Doe", 29 | }, 30 | }; 31 | 32 | test("transformPerson uses name as text value if available", () => { 33 | expect(transformPerson(person)).toMatchInlineSnapshot(` 34 | { 35 | "children": [ 36 | { 37 | "type": "text", 38 | "value": "John Doe", 39 | }, 40 | ], 41 | "properties": { 42 | "href": "mailto:john@example.com", 43 | }, 44 | "tagName": "a", 45 | "type": "element", 46 | } 47 | `); 48 | }); 49 | 50 | test("transformPerson uses email as text value if no name", () => { 51 | expect( 52 | transformPerson({ 53 | ...person, 54 | personProperties: { email: person.personProperties.email }, 55 | }) 56 | ).toMatchInlineSnapshot(` 57 | { 58 | "children": [ 59 | { 60 | "type": "text", 61 | "value": "john@example.com", 62 | }, 63 | ], 64 | "properties": { 65 | "href": "mailto:john@example.com", 66 | }, 67 | "tagName": "a", 68 | "type": "element", 69 | } 70 | `); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /src/hast/paragraph/person.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { h } from "hastscript"; 18 | 19 | import { wrapStyle } from "../common/wrapStyle"; 20 | 21 | import type { docs_v1 } from "@googleapis/docs"; 22 | 23 | export const transformPerson = ( 24 | person: docs_v1.Schema$ParagraphElement["person"] 25 | ) => 26 | wrapStyle( 27 | h( 28 | "a", 29 | { href: `mailto:${person.personProperties.email}` }, 30 | person.personProperties.name ?? person.personProperties.email 31 | ), 32 | person.textStyle 33 | ); 34 | -------------------------------------------------------------------------------- /src/hast/paragraph/richLink.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { describe, expect, test } from "vitest"; 18 | 19 | import { transformRichLink } from "./richLink"; 20 | 21 | import type { docs_v1 } from "@googleapis/docs"; 22 | 23 | describe("richLink", () => { 24 | const richLink: docs_v1.Schema$ParagraphElement["richLink"] = { 25 | richLinkId: "richLinkId", 26 | richLinkProperties: { 27 | title: "Title", 28 | uri: "https://example.com", 29 | }, 30 | }; 31 | 32 | test("transformRichLink generates correct output", () => { 33 | expect(transformRichLink(richLink)).toMatchInlineSnapshot(` 34 | { 35 | "children": [ 36 | { 37 | "type": "text", 38 | "value": "Title", 39 | }, 40 | ], 41 | "properties": { 42 | "href": "https://example.com", 43 | }, 44 | "tagName": "a", 45 | "type": "element", 46 | } 47 | `); 48 | }); 49 | 50 | test("transformRichLink uses uri if no title", () => { 51 | expect( 52 | transformRichLink({ 53 | ...richLink, 54 | richLinkProperties: { uri: richLink.richLinkProperties.uri }, 55 | }) 56 | ).toMatchInlineSnapshot(` 57 | { 58 | "children": [ 59 | { 60 | "type": "text", 61 | "value": "https://example.com", 62 | }, 63 | ], 64 | "properties": { 65 | "href": "https://example.com", 66 | }, 67 | "tagName": "a", 68 | "type": "element", 69 | } 70 | `); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /src/hast/paragraph/richLink.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { h } from "hastscript"; 18 | 19 | import { wrapStyle } from "../common/wrapStyle"; 20 | 21 | import type { docs_v1 } from "@googleapis/docs"; 22 | 23 | export const transformRichLink = ( 24 | richLink: docs_v1.Schema$ParagraphElement["richLink"] 25 | ) => 26 | wrapStyle( 27 | h( 28 | "a", 29 | { href: richLink.richLinkProperties.uri }, 30 | richLink.richLinkProperties.title ?? richLink.richLinkProperties.uri 31 | ), 32 | richLink.textStyle 33 | ); 34 | -------------------------------------------------------------------------------- /src/hast/paragraph/textRun.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { h } from "hastscript"; 18 | 19 | import { wrapStyle } from "../common/wrapStyle"; 20 | 21 | import type { docs_v1 } from "@googleapis/docs"; 22 | import type { Element, Text } from "hast"; 23 | 24 | export const transformTextRun = ( 25 | textRun: docs_v1.Schema$ParagraphElement["textRun"] 26 | ): Element | Text => { 27 | const content = textRun.content 28 | .replace(/\n/g, "") 29 | // maintain whitespace 30 | .replace(/ {2}/g, " \u2002"); 31 | 32 | if (textRun.textStyle.link) { 33 | const { url, bookmarkId, headingId } = textRun.textStyle.link; 34 | let href: string; 35 | 36 | if (url) { 37 | href = url; 38 | } 39 | 40 | if (bookmarkId) { 41 | console.warn( 42 | `Unsupported element: paragraph.textRun.textStyle.link.bookmarkId` 43 | ); 44 | return; 45 | } 46 | 47 | if (headingId) { 48 | href = `#${headingId}`; 49 | } 50 | 51 | return h("a", { href }, content); 52 | } 53 | return wrapStyle({ type: "text", value: content }, textRun.textStyle); 54 | }; 55 | -------------------------------------------------------------------------------- /src/hast/postProcessing/prettyHeaderIds.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import Slugger from "github-slugger"; 17 | import { toString } from "hast-util-to-string"; 18 | import { visit } from "unist-util-visit"; 19 | 20 | import type { Element, Root } from "hast"; 21 | 22 | /** 23 | * Replace all header tag IDs with the slug version and update internal links. 24 | * 25 | * @param root The HAST tree to process. 26 | * @returns The processed HAST tree. 27 | */ 28 | export const replaceHeaderIdsWithSlug = (root: Root): Root => { 29 | const links: { [key: string]: Element[] } = {}; 30 | 31 | // find all internal links and track them 32 | visit(root, "element", (node) => { 33 | if (node.tagName !== "a") { 34 | return; 35 | } 36 | const { href = "" } = node.properties || {}; 37 | 38 | if (typeof href === "string" && href.startsWith("#")) { 39 | if (!links[href.slice(1)]) { 40 | links[href.slice(1)] = []; 41 | } 42 | // remove the # from the src to match id 43 | links[href.slice(1)].push(node); 44 | } 45 | }); 46 | 47 | const slugs = new Slugger(); 48 | 49 | // replace all ids for header tags 50 | visit(root, "element", (node) => { 51 | if ( 52 | node.tagName.match(/^h[1-6]$/) && 53 | typeof node.properties?.id === "string" && 54 | node.properties.id.length > 0 55 | ) { 56 | const slug = slugs.slug(toString(node)); 57 | 58 | //update links with slug 59 | for (const link of links[node.properties.id] ?? []) { 60 | link.properties.src = `#${slug}`; 61 | } 62 | 63 | node.properties.id = slug; 64 | } 65 | }); 66 | 67 | return root; 68 | }; 69 | -------------------------------------------------------------------------------- /src/hast/postProcessing/removeStyles.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { visit } from "unist-util-visit"; 17 | 18 | import type { Root } from "hast"; 19 | 20 | /** 21 | * Replace all header tag IDs with the slug version and update internal links. 22 | * 23 | * @param root The HAST tree to process. 24 | * @returns The processed HAST tree. 25 | */ 26 | export const removeStyles = (root: Root): Root => { 27 | visit(root, "element", (node) => { 28 | // remove spans 29 | if (node.children?.length === 1) { 30 | const child = node.children[0]; 31 | if (child.type === "element" && child.tagName === "span") { 32 | node.children = child.children; 33 | } 34 | } 35 | 36 | // remove styles 37 | delete node.properties?.style; 38 | }); 39 | 40 | return root; 41 | }; 42 | -------------------------------------------------------------------------------- /src/hast/table/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { h } from "hastscript"; 18 | 19 | import { transform } from ".."; 20 | import { borders, rgbColor, serializeStyle } from "../common/style"; 21 | 22 | import type { Context } from ".."; 23 | import type { docs_v1 } from "@googleapis/docs"; 24 | import type { Element } from "hast"; 25 | 26 | export const tableToElement = ( 27 | table: docs_v1.Schema$Table, 28 | context: Context 29 | ): Element => { 30 | const { tableRows } = table; 31 | 32 | const el = h("table"); 33 | 34 | for (const row of tableRows) { 35 | const tr = h("tr"); 36 | for (const cell of row.tableCells) { 37 | const td = h("td", transform(cell.content, context)); 38 | styleTableCell(td, cell.tableCellStyle); 39 | 40 | tr.children.push(td); 41 | } 42 | el.children.push(tr); 43 | } 44 | 45 | return el; 46 | }; 47 | 48 | export const styleTableCell = ( 49 | el: Element, 50 | { 51 | backgroundColor, 52 | borderBottom, 53 | borderLeft, 54 | borderRight, 55 | borderTop, 56 | contentAlignment, 57 | paddingBottom, 58 | paddingLeft, 59 | paddingRight, 60 | paddingTop, 61 | }: docs_v1.Schema$TableCellStyle 62 | ) => { 63 | const style: { [key: string]: string } = {}; 64 | 65 | if (backgroundColor && backgroundColor.color) { 66 | style.backgroundColor = rgbColor(backgroundColor); 67 | } 68 | 69 | Object.assign( 70 | style, 71 | borders({ borderBottom, borderLeft, borderRight, borderTop }) 72 | ); 73 | 74 | if (paddingBottom) { 75 | style.paddingBottom = `${paddingBottom.magnitude}${paddingBottom.unit}`; 76 | } 77 | 78 | if (paddingLeft) { 79 | style.paddingLeft = `${paddingLeft.magnitude}${paddingLeft.unit}`; 80 | } 81 | 82 | if (paddingBottom) { 83 | style.paddingRight = `${paddingRight.magnitude}${paddingRight.unit}`; 84 | } 85 | 86 | if (paddingBottom) { 87 | style.paddingTop = `${paddingTop.magnitude}${paddingTop.unit}`; 88 | } 89 | 90 | if (contentAlignment) { 91 | style.verticalAlign = contentAlignment.toLowerCase(); 92 | } 93 | 94 | if (Object.keys(style).length > 0) { 95 | el.properties.style = serializeStyle(style); 96 | } 97 | }; 98 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2022 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export { hast, toHast, HastOptions } from "./hast"; 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "declaration": true, 5 | "target": "ESNEXT", 6 | "esModuleInterop": true, 7 | "moduleResolution": "node" 8 | }, 9 | "include": ["src/**/*", "e2e/**/*"], 10 | "exclude": ["node_modules"] 11 | } -------------------------------------------------------------------------------- /typedoc.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('typedoc').TypeDocOptions} 3 | */ 4 | module.exports = { 5 | entryPoints: ["src/index.ts"], 6 | navigationLinks: { 7 | "Google Docs API": 8 | "https://developers.google.com/docs/api/reference/rest/v1/documents#Document", 9 | HAST: "https://github.com/syntax-tree/hast", 10 | }, 11 | customCss: "assets/docs.css", 12 | treatWarningsAsErrors: true, 13 | categorizeByGroup: false, 14 | }; 15 | --------------------------------------------------------------------------------