├── .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 | 
2 |
3 | [](https://www.npmjs.com/package/@googleworkspace/google-docs-hast)
4 | [](https://github.com/googleworkspace/google-docs-hast/actions/workflows/test.yml)
5 | 
6 | [](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 | -
103 | Ordered listed bullet
104 |
105 | - Nested ordered list bullet
106 |
107 |
108 | - Another list bullet
109 |
110 | Tables
111 |
112 |
113 |
114 |
123 | 0
124 | 1
125 | 2
126 | |
127 |
136 | top
137 | |
138 |
147 | middle
148 | |
149 |
158 | bottom
159 | |
160 |
161 |
162 |
163 |
164 |
165 |
174 |
175 | A table can contain other objects:
176 |
177 |
178 |
179 |
182 |
183 | |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
201 | Custom borders and padding
202 | |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
218 | A table row
219 | |
220 |
229 | Can have
230 | |
231 |
232 |
233 |
242 | Varying number of columns
243 | |
244 |
253 |
254 | |
255 |
256 |
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 |
308 |
309 | const
325 | foo =
342 | "code block converted with addon";
362 |
363 | |
364 |
365 |
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 | -
60 | Ordered listed bullet
61 |
62 | - Nested ordered list bullet
63 |
64 |
65 | - Another list bullet
66 |
67 | Tables
68 |
69 |
70 |
71 |
72 | 0
73 | 1
74 | 2
75 | |
76 | top |
77 | middle |
78 | bottom |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | A table can contain other objects:
87 |
88 |
89 |
90 |
93 |
94 | |
95 |
96 |
97 |
98 |
99 |
100 |
101 | Custom borders and padding |
102 |
103 |
104 |
105 |
106 |
107 |
108 | A table row |
109 | Can have |
110 |
111 |
112 | Varying number of columns |
113 | |
114 |
115 |
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 |
152 |
153 | const foo = "code block converted with addon";
156 |
157 | |
158 |
159 |
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 | -
106 | Ordered listed bullet
107 |
108 | - Nested ordered list bullet
109 |
110 |
111 | - Another list bullet
112 |
113 | Tables
114 |
115 |
116 |
117 |
126 | 0
127 | 1
128 | 2
129 | |
130 |
139 | top
140 | |
141 |
150 | middle
151 | |
152 |
161 | bottom
162 | |
163 |
164 |
165 |
166 |
167 |
168 |
177 |
178 | A table can contain other objects:
179 |
180 |
181 |
182 |
185 |
186 | |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
204 | Custom borders and padding
205 | |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
221 | A table row
222 | |
223 |
232 | Can have
233 | |
234 |
235 |
236 |
245 | Varying number of columns
246 | |
247 |
256 |
257 | |
258 |
259 |
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 |
311 |
312 | const
328 | foo =
345 | "code block converted with addon";
365 |
366 | |
367 |
368 |
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 | -
559 | Ordered listed bullet
560 |
561 | - Nested ordered list bullet
562 |
563 |
564 | - Another list bullet
565 |
566 | Tables
567 |
568 |
569 |
570 |
571 | 0
572 | 1
573 | 2
574 | |
575 | top |
576 | middle |
577 | bottom |
578 |
579 |
580 |
581 |
582 |
583 |
584 |
585 | A table can contain other objects:
586 |
587 |
588 |
589 |
592 |
593 | |
594 |
595 |
596 |
597 |
598 |
599 |
600 | Custom borders and padding |
601 |
602 |
603 |
604 |
605 |
606 |
607 | A table row |
608 | Can have |
609 |
610 |
611 | Varying number of columns |
612 | |
613 |
614 |
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 |
651 |
652 | const foo = "code block converted with addon";
655 |
656 | |
657 |
658 |
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 |
--------------------------------------------------------------------------------