=} options.defaults
107 | */
108 | const updateFile = ({file, folder}) => {
109 | Object.assign(file, {
110 | exclude: false,
111 | page: true,
112 | index: file.name === "index",
113 | date: file.createdTime,
114 | ...folder.metadata,
115 | })
116 |
117 | // Transform description into metadata if description is YAML
118 | const metadata = getMetadataFromDescription(file.description)
119 | Object.assign(file, metadata)
120 |
121 | // Breadcrumb, slug, path
122 | Object.assign(file, getTreeMetadata(folder.tree, file))
123 |
124 | return file
125 | }
126 |
127 | async function getGoogleDrive() {
128 | const googleOAuth2 = new GoogleOAuth2({
129 | token: ENV_TOKEN_VAR,
130 | })
131 | const auth = await googleOAuth2.getAuth()
132 |
133 | return google.drive({version: "v3", auth})
134 | }
135 |
136 | /**
137 | * @typedef DocumentFetchParent
138 | * @property {string | null} id
139 | * @property {string[]} breadcrumb
140 | * @property {string} path
141 | */
142 |
143 | /**
144 | * @typedef FetchDocumentsOptions
145 | * @property {import('googleapis').drive_v3.Drive} drive
146 | * @property {DocumentFetchParent[]} parents
147 | */
148 |
149 | // 10 per 1.5 seconds.
150 | const rateLimit = wait(10, 1500)
151 | const BATCH_SIZE = 100
152 | /**
153 | * @param {import('..').Options & FetchDocumentsOptions} options
154 | * @returns {Promise<(import('..').DocumentFile & { path: string })[]>}
155 | */
156 | async function fetchDocumentsFiles({drive, parents, options}) {
157 | if (parents.length > BATCH_SIZE) {
158 | return _flatten(
159 | await Promise.all(
160 | evenlyChunk(parents, BATCH_SIZE).map((parents) =>
161 | fetchDocumentsFiles({
162 | drive,
163 | parents,
164 | options,
165 | })
166 | )
167 | )
168 | )
169 | }
170 |
171 | const waited = await rateLimit()
172 | if (options.debug && waited > 1000) {
173 | const waitingTime = (waited / 1000).toFixed(1)
174 | console.info(
175 | `source-google-docs: rate limit reach. waiting ${waitingTime}s`
176 | )
177 | }
178 |
179 | const parentQuery =
180 | parents.length === 1 && parents[0].id === null
181 | ? false
182 | : parents.map((p) => `'${p.id}' in parents`).join(" or ")
183 |
184 | const query = {
185 | includeItemsFromAllDrives: true,
186 | supportsAllDrives: true,
187 | q: `${
188 | parentQuery ? `(${parentQuery}) and ` : ""
189 | }(mimeType='${MIME_TYPE_FOLDER}' or mimeType='${MIME_TYPE_DOCUMENT}') and trashed = false`,
190 | fields: `nextPageToken,files(id, mimeType, name, description, createdTime, modifiedTime, starred, parents)`,
191 | }
192 |
193 | const res = await drive.files.list(query)
194 |
195 | /** @param {typeof res.data.files} files */
196 | const collectDocuments = (files) =>
197 | files
198 | .filter(
199 | /** @returns {file is import("..").DocumentFile} */
200 | (file) => file.mimeType === MIME_TYPE_DOCUMENT
201 | )
202 | .map((file) => {
203 | const parentIds = file.parents && new Set(file.parents)
204 | const folder = parentIds && parents.find((p) => parentIds.has(p.id))
205 | return updateFile({
206 | folder,
207 | file,
208 | })
209 | })
210 | .filter((file) => !file.exclude)
211 | let documents = collectDocuments(res.data.files)
212 |
213 | /** @param {typeof res.data.files} files */
214 | const collectParents = (files) => {
215 | return files
216 | .filter((file) => file.mimeType === MIME_TYPE_FOLDER)
217 | .map((folder) => {
218 | const parentIds = folder.parents && new Set(folder.parents)
219 | const parent = parentIds && parents.find((p) => parentIds.has(p.id))
220 | const metadata = getMetadataFromDescription(folder.description)
221 | const tree = [
222 | ...parent.tree,
223 | {
224 | name: folder.name,
225 | skip: metadata.skip || false,
226 | },
227 | ]
228 |
229 | // we don't want to spread "skip" folder metadata to documents
230 | if (metadata.skip) {
231 | delete metadata.skip
232 | }
233 |
234 | return {
235 | id: folder.id,
236 | tree,
237 | metadata: {
238 | ...parent.metadata,
239 | ...metadata,
240 | },
241 | }
242 | })
243 | .filter((folder) => !folder.exclude)
244 | }
245 | let nextParents = collectParents(res.data.files)
246 |
247 | if (!res.data.nextPageToken) {
248 | if (nextParents.length === 0) {
249 | return documents
250 | }
251 | const documentsInFolders = await fetchDocumentsFiles({
252 | drive,
253 | parents: nextParents,
254 | options,
255 | })
256 | return [...documents, ...documentsInFolders]
257 | }
258 |
259 | /** @type {typeof documents} */
260 | let documentsInFolders = []
261 |
262 | const fetchOneParentsBatch = async () => {
263 | // process one batch of children while continuing on with pages
264 | const parentBatch = nextParents.slice(0, BATCH_SIZE)
265 | nextParents = nextParents.slice(BATCH_SIZE)
266 | const results = await fetchDocumentsFiles({
267 | drive,
268 | parents: parentBatch,
269 | options,
270 | })
271 | documentsInFolders = [...documentsInFolders, ...results]
272 | }
273 |
274 | /** @param {string} nextPageToken */
275 | const fetchNextPage = async (nextPageToken) => {
276 | await rateLimit()
277 | const nextRes = await drive.files.list({
278 | ...query,
279 | pageToken: nextPageToken,
280 | })
281 | documents = [...documents, ...collectDocuments(nextRes.data.files)]
282 | nextParents = [...nextParents, ...collectParents(nextRes.data.files)]
283 |
284 | if (!nextRes.data.nextPageToken) {
285 | if (nextParents.length === 0) {
286 | return documents
287 | }
288 | const finalDocumentsInFolders = await fetchDocumentsFiles({
289 | drive,
290 | parents: nextParents,
291 | options,
292 | })
293 | return [...documents, ...documentsInFolders, ...finalDocumentsInFolders]
294 | }
295 |
296 | const nextPagePromise = fetchNextPage(nextRes.data.nextPageToken)
297 | if (nextParents.length < BATCH_SIZE) {
298 | return nextPagePromise
299 | }
300 | return (await Promise.all([nextPagePromise, fetchOneParentsBatch()]))[0]
301 | }
302 | return fetchNextPage(res.data.nextPageToken)
303 | }
304 |
305 | /** @param {import('..').Options} pluginOptions */
306 | async function fetchFiles({folder, ...options}) {
307 | const drive = await getGoogleDrive()
308 |
309 | const res = await drive.files.get({
310 | fileId: folder,
311 | fields: "description",
312 | supportsAllDrives: true,
313 | })
314 |
315 | const documentsFiles = await fetchDocumentsFiles({
316 | drive,
317 | parents: [
318 | {
319 | id: folder,
320 | tree: [],
321 | metadata: getMetadataFromDescription(res.data.description),
322 | },
323 | ],
324 | options,
325 | })
326 |
327 | return documentsFiles
328 | }
329 |
330 | module.exports = {
331 | fetchFiles,
332 | }
333 |
--------------------------------------------------------------------------------
/__tests__/documents/empty.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Empty",
3 | "body": {
4 | "content": [
5 | {
6 | "endIndex": 1,
7 | "sectionBreak": {
8 | "sectionStyle": {
9 | "columnSeparatorStyle": "NONE",
10 | "contentDirection": "LEFT_TO_RIGHT",
11 | "sectionType": "CONTINUOUS"
12 | }
13 | }
14 | },
15 | {
16 | "startIndex": 1,
17 | "endIndex": 2,
18 | "paragraph": {
19 | "elements": [
20 | {
21 | "startIndex": 1,
22 | "endIndex": 2,
23 | "textRun": {"content": "\n", "textStyle": {}}
24 | }
25 | ],
26 | "paragraphStyle": {
27 | "namedStyleType": "NORMAL_TEXT",
28 | "direction": "LEFT_TO_RIGHT"
29 | }
30 | }
31 | },
32 | {
33 | "startIndex": 2,
34 | "endIndex": 3,
35 | "paragraph": {
36 | "elements": [
37 | {
38 | "startIndex": 2,
39 | "endIndex": 3,
40 | "textRun": {"content": "", "textStyle": {}}
41 | }
42 | ],
43 | "paragraphStyle": {
44 | "headingId": "h.4u3anmqa57uf",
45 | "namedStyleType": "HEADING_1",
46 | "direction": "LEFT_TO_RIGHT",
47 | "avoidWidowAndOrphan": true
48 | }
49 | }
50 | }
51 | ]
52 | },
53 | "documentStyle": {
54 | "background": {"color": {}},
55 | "pageNumberStart": 1,
56 | "marginTop": {"magnitude": 72, "unit": "PT"},
57 | "marginBottom": {"magnitude": 72, "unit": "PT"},
58 | "marginRight": {"magnitude": 72, "unit": "PT"},
59 | "marginLeft": {"magnitude": 72, "unit": "PT"},
60 | "pageSize": {
61 | "height": {"magnitude": 841.8897637795277, "unit": "PT"},
62 | "width": {"magnitude": 595.2755905511812, "unit": "PT"}
63 | },
64 | "marginHeader": {"magnitude": 36, "unit": "PT"},
65 | "marginFooter": {"magnitude": 36, "unit": "PT"},
66 | "useCustomHeaderFooterMargins": true
67 | },
68 | "namedStyles": {
69 | "styles": [
70 | {
71 | "namedStyleType": "NORMAL_TEXT",
72 | "textStyle": {
73 | "bold": false,
74 | "italic": false,
75 | "underline": false,
76 | "strikethrough": false,
77 | "smallCaps": false,
78 | "backgroundColor": {},
79 | "foregroundColor": {"color": {"rgbColor": {}}},
80 | "fontSize": {"magnitude": 11, "unit": "PT"},
81 | "weightedFontFamily": {"fontFamily": "Arial", "weight": 400},
82 | "baselineOffset": "NONE"
83 | },
84 | "paragraphStyle": {
85 | "namedStyleType": "NORMAL_TEXT",
86 | "alignment": "START",
87 | "lineSpacing": 115,
88 | "direction": "LEFT_TO_RIGHT",
89 | "spacingMode": "COLLAPSE_LISTS",
90 | "spaceAbove": {"unit": "PT"},
91 | "spaceBelow": {"unit": "PT"},
92 | "borderBetween": {
93 | "color": {},
94 | "width": {"unit": "PT"},
95 | "padding": {"unit": "PT"},
96 | "dashStyle": "SOLID"
97 | },
98 | "borderTop": {
99 | "color": {},
100 | "width": {"unit": "PT"},
101 | "padding": {"unit": "PT"},
102 | "dashStyle": "SOLID"
103 | },
104 | "borderBottom": {
105 | "color": {},
106 | "width": {"unit": "PT"},
107 | "padding": {"unit": "PT"},
108 | "dashStyle": "SOLID"
109 | },
110 | "borderLeft": {
111 | "color": {},
112 | "width": {"unit": "PT"},
113 | "padding": {"unit": "PT"},
114 | "dashStyle": "SOLID"
115 | },
116 | "borderRight": {
117 | "color": {},
118 | "width": {"unit": "PT"},
119 | "padding": {"unit": "PT"},
120 | "dashStyle": "SOLID"
121 | },
122 | "indentFirstLine": {"unit": "PT"},
123 | "indentStart": {"unit": "PT"},
124 | "indentEnd": {"unit": "PT"},
125 | "keepLinesTogether": false,
126 | "keepWithNext": false,
127 | "avoidWidowAndOrphan": false,
128 | "shading": {"backgroundColor": {}}
129 | }
130 | },
131 | {
132 | "namedStyleType": "HEADING_1",
133 | "textStyle": {
134 | "bold": true,
135 | "foregroundColor": {
136 | "color": {
137 | "rgbColor": {
138 | "red": 0.10980392,
139 | "green": 0.27058825,
140 | "blue": 0.5294118
141 | }
142 | }
143 | },
144 | "fontSize": {"magnitude": 16, "unit": "PT"},
145 | "weightedFontFamily": {"fontFamily": "Trebuchet MS", "weight": 400}
146 | },
147 | "paragraphStyle": {
148 | "headingId": "h.4u3anmqa57uf",
149 | "namedStyleType": "HEADING_1",
150 | "direction": "LEFT_TO_RIGHT",
151 | "spacingMode": "COLLAPSE_LISTS",
152 | "spaceAbove": {"magnitude": 10, "unit": "PT"},
153 | "indentFirstLine": {"magnitude": 36, "unit": "PT"},
154 | "indentStart": {"magnitude": 36, "unit": "PT"},
155 | "keepLinesTogether": false,
156 | "keepWithNext": false,
157 | "avoidWidowAndOrphan": false
158 | }
159 | },
160 | {
161 | "namedStyleType": "HEADING_2",
162 | "textStyle": {
163 | "bold": true,
164 | "foregroundColor": {
165 | "color": {
166 | "rgbColor": {
167 | "red": 0.23921569,
168 | "green": 0.52156866,
169 | "blue": 0.7764706
170 | }
171 | }
172 | },
173 | "fontSize": {"magnitude": 13, "unit": "PT"},
174 | "weightedFontFamily": {"fontFamily": "Trebuchet MS", "weight": 400}
175 | },
176 | "paragraphStyle": {
177 | "headingId": "h.yh37ranpban7",
178 | "namedStyleType": "HEADING_2",
179 | "direction": "LEFT_TO_RIGHT",
180 | "spacingMode": "COLLAPSE_LISTS",
181 | "spaceAbove": {"magnitude": 10, "unit": "PT"},
182 | "indentFirstLine": {"magnitude": 72, "unit": "PT"},
183 | "indentStart": {"magnitude": 72, "unit": "PT"},
184 | "keepLinesTogether": false,
185 | "keepWithNext": false,
186 | "avoidWidowAndOrphan": false
187 | }
188 | },
189 | {
190 | "namedStyleType": "HEADING_3",
191 | "textStyle": {
192 | "bold": true,
193 | "foregroundColor": {
194 | "color": {
195 | "rgbColor": {
196 | "red": 0.42745098,
197 | "green": 0.61960787,
198 | "blue": 0.92156863
199 | }
200 | }
201 | },
202 | "fontSize": {"magnitude": 12, "unit": "PT"},
203 | "weightedFontFamily": {"fontFamily": "Trebuchet MS", "weight": 400}
204 | },
205 | "paragraphStyle": {
206 | "headingId": "h.x2p4yxmolykq",
207 | "namedStyleType": "HEADING_3",
208 | "direction": "LEFT_TO_RIGHT",
209 | "spacingMode": "COLLAPSE_LISTS",
210 | "spaceAbove": {"magnitude": 8, "unit": "PT"},
211 | "indentFirstLine": {"magnitude": 36, "unit": "PT"},
212 | "indentStart": {"magnitude": 36, "unit": "PT"},
213 | "keepLinesTogether": false,
214 | "keepWithNext": false,
215 | "avoidWidowAndOrphan": false
216 | }
217 | },
218 | {
219 | "namedStyleType": "HEADING_4",
220 | "textStyle": {
221 | "underline": true,
222 | "foregroundColor": {
223 | "color": {"rgbColor": {"red": 0.4, "green": 0.4, "blue": 0.4}}
224 | },
225 | "fontSize": {"magnitude": 11, "unit": "PT"},
226 | "weightedFontFamily": {"fontFamily": "Trebuchet MS", "weight": 400}
227 | },
228 | "paragraphStyle": {
229 | "namedStyleType": "NORMAL_TEXT",
230 | "direction": "LEFT_TO_RIGHT",
231 | "spacingMode": "COLLAPSE_LISTS",
232 | "spaceAbove": {"magnitude": 8, "unit": "PT"},
233 | "spaceBelow": {"unit": "PT"},
234 | "keepLinesTogether": false,
235 | "keepWithNext": false,
236 | "avoidWidowAndOrphan": false
237 | }
238 | },
239 | {
240 | "namedStyleType": "HEADING_5",
241 | "textStyle": {
242 | "foregroundColor": {
243 | "color": {"rgbColor": {"red": 0.4, "green": 0.4, "blue": 0.4}}
244 | },
245 | "fontSize": {"magnitude": 11, "unit": "PT"},
246 | "weightedFontFamily": {"fontFamily": "Trebuchet MS", "weight": 400}
247 | },
248 | "paragraphStyle": {
249 | "namedStyleType": "NORMAL_TEXT",
250 | "direction": "LEFT_TO_RIGHT",
251 | "spacingMode": "COLLAPSE_LISTS",
252 | "spaceAbove": {"magnitude": 8, "unit": "PT"},
253 | "spaceBelow": {"unit": "PT"},
254 | "keepLinesTogether": false,
255 | "keepWithNext": false,
256 | "avoidWidowAndOrphan": false
257 | }
258 | },
259 | {
260 | "namedStyleType": "HEADING_6",
261 | "textStyle": {
262 | "italic": true,
263 | "foregroundColor": {
264 | "color": {"rgbColor": {"red": 0.4, "green": 0.4, "blue": 0.4}}
265 | },
266 | "fontSize": {"magnitude": 11, "unit": "PT"},
267 | "weightedFontFamily": {"fontFamily": "Trebuchet MS", "weight": 400}
268 | },
269 | "paragraphStyle": {
270 | "namedStyleType": "NORMAL_TEXT",
271 | "direction": "LEFT_TO_RIGHT",
272 | "spacingMode": "COLLAPSE_LISTS",
273 | "spaceAbove": {"magnitude": 8, "unit": "PT"},
274 | "spaceBelow": {"unit": "PT"},
275 | "keepLinesTogether": false,
276 | "keepWithNext": false,
277 | "avoidWidowAndOrphan": false
278 | }
279 | },
280 | {
281 | "namedStyleType": "TITLE",
282 | "textStyle": {
283 | "fontSize": {"magnitude": 21, "unit": "PT"},
284 | "weightedFontFamily": {"fontFamily": "Trebuchet MS", "weight": 400}
285 | },
286 | "paragraphStyle": {
287 | "namedStyleType": "NORMAL_TEXT",
288 | "direction": "LEFT_TO_RIGHT",
289 | "spacingMode": "COLLAPSE_LISTS",
290 | "spaceAbove": {"unit": "PT"},
291 | "spaceBelow": {"unit": "PT"},
292 | "keepLinesTogether": false,
293 | "keepWithNext": false,
294 | "avoidWidowAndOrphan": false
295 | }
296 | },
297 | {
298 | "namedStyleType": "SUBTITLE",
299 | "textStyle": {
300 | "italic": true,
301 | "foregroundColor": {
302 | "color": {"rgbColor": {"red": 0.4, "green": 0.4, "blue": 0.4}}
303 | },
304 | "fontSize": {"magnitude": 13, "unit": "PT"},
305 | "weightedFontFamily": {"fontFamily": "Trebuchet MS", "weight": 400}
306 | },
307 | "paragraphStyle": {
308 | "namedStyleType": "NORMAL_TEXT",
309 | "direction": "LEFT_TO_RIGHT",
310 | "spacingMode": "COLLAPSE_LISTS",
311 | "spaceAbove": {"unit": "PT"},
312 | "spaceBelow": {"magnitude": 10, "unit": "PT"},
313 | "keepLinesTogether": false,
314 | "keepWithNext": false,
315 | "avoidWidowAndOrphan": false
316 | }
317 | }
318 | ]
319 | },
320 | "revisionId": "ALm37BVb1qhhqmHfXfwaXtPOqtAYzP3wDZLeiRq-OvGKaONFNOSkbw9F12Q3uAsj1oCPXF7vPf7fDefXBXxPAg",
321 | "suggestionsViewMode": "SUGGESTIONS_INLINE",
322 | "documentId": "1OEyYKhzFz3pcFQMmI8i8Uqs1knm_AIAQJli1hkj5CWA"
323 | }
324 |
--------------------------------------------------------------------------------
/__tests__/documents/special-chars.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Special characters",
3 | "body": {
4 | "content": [
5 | {
6 | "endIndex": 1,
7 | "sectionBreak": {
8 | "sectionStyle": {
9 | "columnSeparatorStyle": "NONE",
10 | "contentDirection": "LEFT_TO_RIGHT",
11 | "sectionType": "CONTINUOUS"
12 | }
13 | }
14 | },
15 | {
16 | "startIndex": 1,
17 | "endIndex": 20,
18 | "paragraph": {
19 | "elements": [
20 | {
21 | "startIndex": 1,
22 | "endIndex": 19,
23 | "textRun": {
24 | "content": "19980006564 - 0081",
25 | "textStyle": {
26 | "backgroundColor": {
27 | "color": {"rgbColor": {"red": 1, "green": 1, "blue": 1}}
28 | },
29 | "foregroundColor": {
30 | "color": {
31 | "rgbColor": {"red": 0.2, "green": 0.2, "blue": 0.2}
32 | }
33 | },
34 | "weightedFontFamily": {
35 | "fontFamily": "Times New Roman",
36 | "weight": 400
37 | }
38 | }
39 | }
40 | },
41 | {
42 | "startIndex": 19,
43 | "endIndex": 20,
44 | "textRun": {"content": "\n", "textStyle": {}}
45 | }
46 | ],
47 | "paragraphStyle": {
48 | "namedStyleType": "NORMAL_TEXT",
49 | "direction": "LEFT_TO_RIGHT",
50 | "avoidWidowAndOrphan": true
51 | }
52 | }
53 | }
54 | ]
55 | },
56 | "documentStyle": {
57 | "background": {"color": {}},
58 | "pageNumberStart": 1,
59 | "marginTop": {"magnitude": 72, "unit": "PT"},
60 | "marginBottom": {"magnitude": 72, "unit": "PT"},
61 | "marginRight": {"magnitude": 72, "unit": "PT"},
62 | "marginLeft": {"magnitude": 72, "unit": "PT"},
63 | "pageSize": {
64 | "height": {"magnitude": 841.8897637795277, "unit": "PT"},
65 | "width": {"magnitude": 595.2755905511812, "unit": "PT"}
66 | },
67 | "marginHeader": {"magnitude": 36, "unit": "PT"},
68 | "marginFooter": {"magnitude": 36, "unit": "PT"},
69 | "useCustomHeaderFooterMargins": true
70 | },
71 | "namedStyles": {
72 | "styles": [
73 | {
74 | "namedStyleType": "NORMAL_TEXT",
75 | "textStyle": {
76 | "bold": false,
77 | "italic": false,
78 | "underline": false,
79 | "strikethrough": false,
80 | "smallCaps": false,
81 | "backgroundColor": {},
82 | "foregroundColor": {"color": {"rgbColor": {}}},
83 | "fontSize": {"magnitude": 11, "unit": "PT"},
84 | "weightedFontFamily": {"fontFamily": "Arial", "weight": 400},
85 | "baselineOffset": "NONE"
86 | },
87 | "paragraphStyle": {
88 | "namedStyleType": "NORMAL_TEXT",
89 | "alignment": "START",
90 | "lineSpacing": 115,
91 | "direction": "LEFT_TO_RIGHT",
92 | "spacingMode": "COLLAPSE_LISTS",
93 | "spaceAbove": {"unit": "PT"},
94 | "spaceBelow": {"unit": "PT"},
95 | "borderBetween": {
96 | "color": {},
97 | "width": {"unit": "PT"},
98 | "padding": {"unit": "PT"},
99 | "dashStyle": "SOLID"
100 | },
101 | "borderTop": {
102 | "color": {},
103 | "width": {"unit": "PT"},
104 | "padding": {"unit": "PT"},
105 | "dashStyle": "SOLID"
106 | },
107 | "borderBottom": {
108 | "color": {},
109 | "width": {"unit": "PT"},
110 | "padding": {"unit": "PT"},
111 | "dashStyle": "SOLID"
112 | },
113 | "borderLeft": {
114 | "color": {},
115 | "width": {"unit": "PT"},
116 | "padding": {"unit": "PT"},
117 | "dashStyle": "SOLID"
118 | },
119 | "borderRight": {
120 | "color": {},
121 | "width": {"unit": "PT"},
122 | "padding": {"unit": "PT"},
123 | "dashStyle": "SOLID"
124 | },
125 | "indentFirstLine": {"unit": "PT"},
126 | "indentStart": {"unit": "PT"},
127 | "indentEnd": {"unit": "PT"},
128 | "keepLinesTogether": false,
129 | "keepWithNext": false,
130 | "avoidWidowAndOrphan": false,
131 | "shading": {"backgroundColor": {}}
132 | }
133 | },
134 | {
135 | "namedStyleType": "HEADING_1",
136 | "textStyle": {
137 | "bold": true,
138 | "foregroundColor": {
139 | "color": {
140 | "rgbColor": {
141 | "red": 0.10980392,
142 | "green": 0.27058825,
143 | "blue": 0.5294118
144 | }
145 | }
146 | },
147 | "fontSize": {"magnitude": 16, "unit": "PT"},
148 | "weightedFontFamily": {"fontFamily": "Trebuchet MS", "weight": 400}
149 | },
150 | "paragraphStyle": {
151 | "headingId": "h.4u3anmqa57uf",
152 | "namedStyleType": "HEADING_1",
153 | "direction": "LEFT_TO_RIGHT",
154 | "spacingMode": "COLLAPSE_LISTS",
155 | "spaceAbove": {"magnitude": 10, "unit": "PT"},
156 | "indentFirstLine": {"magnitude": 36, "unit": "PT"},
157 | "indentStart": {"magnitude": 36, "unit": "PT"},
158 | "keepLinesTogether": false,
159 | "keepWithNext": false,
160 | "avoidWidowAndOrphan": false
161 | }
162 | },
163 | {
164 | "namedStyleType": "HEADING_2",
165 | "textStyle": {
166 | "bold": true,
167 | "foregroundColor": {
168 | "color": {
169 | "rgbColor": {
170 | "red": 0.23921569,
171 | "green": 0.52156866,
172 | "blue": 0.7764706
173 | }
174 | }
175 | },
176 | "fontSize": {"magnitude": 13, "unit": "PT"},
177 | "weightedFontFamily": {"fontFamily": "Trebuchet MS", "weight": 400}
178 | },
179 | "paragraphStyle": {
180 | "headingId": "h.yh37ranpban7",
181 | "namedStyleType": "HEADING_2",
182 | "direction": "LEFT_TO_RIGHT",
183 | "spacingMode": "COLLAPSE_LISTS",
184 | "spaceAbove": {"magnitude": 10, "unit": "PT"},
185 | "indentFirstLine": {"magnitude": 72, "unit": "PT"},
186 | "indentStart": {"magnitude": 72, "unit": "PT"},
187 | "keepLinesTogether": false,
188 | "keepWithNext": false,
189 | "avoidWidowAndOrphan": false
190 | }
191 | },
192 | {
193 | "namedStyleType": "HEADING_3",
194 | "textStyle": {
195 | "bold": true,
196 | "foregroundColor": {
197 | "color": {
198 | "rgbColor": {
199 | "red": 0.42745098,
200 | "green": 0.61960787,
201 | "blue": 0.92156863
202 | }
203 | }
204 | },
205 | "fontSize": {"magnitude": 12, "unit": "PT"},
206 | "weightedFontFamily": {"fontFamily": "Trebuchet MS", "weight": 400}
207 | },
208 | "paragraphStyle": {
209 | "headingId": "h.x2p4yxmolykq",
210 | "namedStyleType": "HEADING_3",
211 | "direction": "LEFT_TO_RIGHT",
212 | "spacingMode": "COLLAPSE_LISTS",
213 | "spaceAbove": {"magnitude": 8, "unit": "PT"},
214 | "indentFirstLine": {"magnitude": 36, "unit": "PT"},
215 | "indentStart": {"magnitude": 36, "unit": "PT"},
216 | "keepLinesTogether": false,
217 | "keepWithNext": false,
218 | "avoidWidowAndOrphan": false
219 | }
220 | },
221 | {
222 | "namedStyleType": "HEADING_4",
223 | "textStyle": {
224 | "underline": true,
225 | "foregroundColor": {
226 | "color": {"rgbColor": {"red": 0.4, "green": 0.4, "blue": 0.4}}
227 | },
228 | "fontSize": {"magnitude": 11, "unit": "PT"},
229 | "weightedFontFamily": {"fontFamily": "Trebuchet MS", "weight": 400}
230 | },
231 | "paragraphStyle": {
232 | "namedStyleType": "NORMAL_TEXT",
233 | "direction": "LEFT_TO_RIGHT",
234 | "spacingMode": "COLLAPSE_LISTS",
235 | "spaceAbove": {"magnitude": 8, "unit": "PT"},
236 | "spaceBelow": {"unit": "PT"},
237 | "keepLinesTogether": false,
238 | "keepWithNext": false,
239 | "avoidWidowAndOrphan": false
240 | }
241 | },
242 | {
243 | "namedStyleType": "HEADING_5",
244 | "textStyle": {
245 | "foregroundColor": {
246 | "color": {"rgbColor": {"red": 0.4, "green": 0.4, "blue": 0.4}}
247 | },
248 | "fontSize": {"magnitude": 11, "unit": "PT"},
249 | "weightedFontFamily": {"fontFamily": "Trebuchet MS", "weight": 400}
250 | },
251 | "paragraphStyle": {
252 | "namedStyleType": "NORMAL_TEXT",
253 | "direction": "LEFT_TO_RIGHT",
254 | "spacingMode": "COLLAPSE_LISTS",
255 | "spaceAbove": {"magnitude": 8, "unit": "PT"},
256 | "spaceBelow": {"unit": "PT"},
257 | "keepLinesTogether": false,
258 | "keepWithNext": false,
259 | "avoidWidowAndOrphan": false
260 | }
261 | },
262 | {
263 | "namedStyleType": "HEADING_6",
264 | "textStyle": {
265 | "italic": true,
266 | "foregroundColor": {
267 | "color": {"rgbColor": {"red": 0.4, "green": 0.4, "blue": 0.4}}
268 | },
269 | "fontSize": {"magnitude": 11, "unit": "PT"},
270 | "weightedFontFamily": {"fontFamily": "Trebuchet MS", "weight": 400}
271 | },
272 | "paragraphStyle": {
273 | "namedStyleType": "NORMAL_TEXT",
274 | "direction": "LEFT_TO_RIGHT",
275 | "spacingMode": "COLLAPSE_LISTS",
276 | "spaceAbove": {"magnitude": 8, "unit": "PT"},
277 | "spaceBelow": {"unit": "PT"},
278 | "keepLinesTogether": false,
279 | "keepWithNext": false,
280 | "avoidWidowAndOrphan": false
281 | }
282 | },
283 | {
284 | "namedStyleType": "TITLE",
285 | "textStyle": {
286 | "fontSize": {"magnitude": 21, "unit": "PT"},
287 | "weightedFontFamily": {"fontFamily": "Trebuchet MS", "weight": 400}
288 | },
289 | "paragraphStyle": {
290 | "namedStyleType": "NORMAL_TEXT",
291 | "direction": "LEFT_TO_RIGHT",
292 | "spacingMode": "COLLAPSE_LISTS",
293 | "spaceAbove": {"unit": "PT"},
294 | "spaceBelow": {"unit": "PT"},
295 | "keepLinesTogether": false,
296 | "keepWithNext": false,
297 | "avoidWidowAndOrphan": false
298 | }
299 | },
300 | {
301 | "namedStyleType": "SUBTITLE",
302 | "textStyle": {
303 | "italic": true,
304 | "foregroundColor": {
305 | "color": {"rgbColor": {"red": 0.4, "green": 0.4, "blue": 0.4}}
306 | },
307 | "fontSize": {"magnitude": 13, "unit": "PT"},
308 | "weightedFontFamily": {"fontFamily": "Trebuchet MS", "weight": 400}
309 | },
310 | "paragraphStyle": {
311 | "namedStyleType": "NORMAL_TEXT",
312 | "direction": "LEFT_TO_RIGHT",
313 | "spacingMode": "COLLAPSE_LISTS",
314 | "spaceAbove": {"unit": "PT"},
315 | "spaceBelow": {"magnitude": 10, "unit": "PT"},
316 | "keepLinesTogether": false,
317 | "keepWithNext": false,
318 | "avoidWidowAndOrphan": false
319 | }
320 | }
321 | ]
322 | },
323 | "revisionId": "ALm37BUU25s-rHtMUlnUj9ro0jBXgcyjya398DVLlgxcKa3sg1ObBSj8XkdZ_lRor9TNteOujkFmRgcQ0r8HMg",
324 | "suggestionsViewMode": "SUGGESTIONS_INLINE",
325 | "documentId": "17hER2IFEwyIiliW1DGT3OOHaJrX_7mVyfID6HcLYib8"
326 | }
327 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
gatsby-source-google-docs
3 |
4 |
5 |
6 |
7 |
8 |
9 | [![Npm][badge-npm]][npm]
10 | [![Build Status][badge-build]][travis]
11 | [![Coverage][badge-coverage]][codecov]
12 | [![Downloads][badge-downloads]][npm]
13 | [![PRs welcome][badge-prs]](#contributing)
14 | [![MIT license][badge-licence]](./LICENCE.md)
15 | [![Paypal][badge-paypal]](https://paypal.me/cedricdelpoux)
16 |
17 |
18 |
19 | ---
20 |
21 | `gatsby-source-google-docs` is a [Gatsby](https://www.gatsbyjs.org/) plugin to use [Google Docs](https://docs.google.com/) as a data source.
22 |
23 | Why use Google Docs to write your content ?
24 |
25 | - 🖋 Best online WYSIWYG editor
26 | - 🖥 Desktop web app
27 | - 📱 Mobile app
28 | - 🛩 Offline redaction
29 | - 🔥 No need for external CMS
30 | - ✅ No more content in your source code
31 |
32 |
33 |
34 | ## Features
35 |
36 | - **Google Docs** formatting options (headings, bullets, tables, images...)
37 | - `MDX` support to use `` in your documents
38 | - **Gatsby** v3 & v4 support
39 | - `gatsby-plugin-image` and `gatsby-image` support
40 | - Code blocs support
41 | - **Gatsby Cloud** support
42 | - Slug generation from **Google Drive** tree
43 | - Crosslinks between pages
44 | - Related content
45 | - Custom metadata to enhance documents
46 |
47 | ## Documentation
48 |
49 | To preview what you can do, please checkout [the documentation website](https://cedricdelpoux.github.io/gatsby-source-google-docs/).
50 |
51 | - 👨🏻💻 [Source code](/examples/website)
52 | - 🗂 [Google Docs content](https://drive.google.com/drive/folders/1YJWX_FRoVusp-51ztedm6HSZqpbJA3ag)
53 |
54 | > 💯 100% content of the website is from Google Docs. Please suggest edits to improve it.
55 |
56 | ## Installation
57 |
58 | Download `gatsby-source-google-docs` and `gatsby-transformer-remark` (or `gatsby-plugin-mdx` for [advanced usage](/examples/website))
59 |
60 | ```shell
61 | yarn add gatsby-source-google-docs gatsby-transformer-remark
62 | ```
63 |
64 | - `gatsby-source-google-docs` transform **Google Docs** to **Markdown**
65 | - `gatsby-transformer-remark` transform **Markdown** to **HTML**
66 | - `gatsby-plugin-mdx` transform **Markdown** to **MDX**
67 |
68 | ## Token generation
69 |
70 | The package needs 3 `.env` variables.
71 |
72 | Preview variables
73 |
74 | ```dotenv
75 | GOOGLE_OAUTH_CLIENT_ID=2...m.apps.googleusercontent.com
76 | GOOGLE_OAUTH_CLIENT_SECRET=Q...axL
77 | GOOGLE_DOCS_TOKEN={"access_token":"ya...J0","refresh_token":"1..mE","scope":"https://www.googleapis.com/auth/drive.metadata.readonly https://www.googleapis.com/auth/documents.readonly","token_type":"Bearer","expiry_date":1598284554759}
78 | ```
79 |
80 |
81 |
82 | `gatsby-source-google-docs` expose a script to generate it.
83 |
84 | - Open a terminal at the root of your project
85 | - Type the following command
86 |
87 | ```shell
88 | npx gatsby-source-google-docs-token
89 | ```
90 |
91 | ## Usage
92 |
93 | ### Organize your documents
94 |
95 | Go to your [Google Drive](https://drive.google.com/drive/), create a folder and put some documents inside it.
96 |
97 | ```js
98 | ↳ 🗂 Root folder `template: page`
99 | ↳ 🗂 en `locale: en` `skip: true`
100 | ↳ 📝 Home `template: home`
101 | ↳ 📝 About
102 | ↳ 🗂 Posts `template: post`
103 | ↳ 🗂 Drafts `exclude: true`
104 | ↳ 📝 Draft 1
105 | ↳ 📝 My year 2020 `date: 2021-01-01`
106 | ↳ 📝 Post 2 `slug: /custom/slug` `template: special-post`
107 | ↳ 🗂 fr `locale: fr`
108 | ↳ 📝 Accueil `template: home`
109 | ```
110 |
111 | 🤡 How to enhance documents with metadata?
112 |
113 | - Fill the document (or folder) `description` field in Google Drive with a `YAML` object
114 |
115 | ```yaml
116 | locale: fr
117 | template: post
118 | category: Category Name
119 | tags: [tag1, tag2]
120 | slug: /custom-slug
121 | date: 2019-01-01
122 | ```
123 |
124 | > There are special metadata
125 | >
126 | > - For folders:
127 | > - `exclude: true`: Exclude the folder and its documents
128 | > - `skip: true`: Remove the folder from slug but keep its documents
129 | > - For documents:
130 | > - `index:true`: Use document as the folder index
131 | > - `page: false`: Prevent page creation when `createPages` option is set to `true`
132 |
133 | - Spread metadata into the tree using folders metadata.
134 |
135 | > ⬆️ For the tree example above:
136 | >
137 | > - Every node will have `template: page` defined as default excepts if you redefine it later.
138 | > - You need to create 3 different templates: `page` (default), `home`, `post`. [Checkout the example template](./example/src/templates/page.js)
139 | > - "en" folder will be removed from slug because of `skip: true`
140 |
141 | - Exclude folders and documents using `exclude: true`. Perfect to keep drafts documents. One you want to publish a page, juste move the document one level up.
142 |
143 | > ⬆️ For the tree example above:
144 | >
145 | > - Documents under `Drafts` will be exclude because of `exclude: true`.
146 |
147 | - Every metadata will be available in `GoogleDocs` nodes and you can use everywhere in you `Gatsby` site
148 |
149 |
150 |
151 | 🌄 How to add cover?
152 |
153 | Add an image in your [Google Document first page header](https://support.google.com/docs/answer/86629)
154 |
155 |
156 |
157 | 🍞 How to add slug and breadcrumb?
158 |
159 | `slug` and `breadcrumb` fields add automatically generated using the folders tree structure and transformed using `kebab-case`.
160 |
161 | > ⬆️ For the tree example above:
162 | > The `GoogleDocs` node for document `My year 2020`
163 | >
164 | > ```js
165 | > {
166 | > path: "/en/posts/my-year-2020" // Original Google Drive path
167 | > slug: "/posts/my-year-2020" // `en` is out because of `skip: true`
168 | > breadcrumb: [
169 | > {name: "Posts", slug: "/posts"},
170 | > {name: "My year 2020", slug: "/posts/my-year-2020"},
171 | > ],
172 | > template: "post" ,// src/templates/post.js
173 | > locale: "fr",
174 | > date: "2021-01-01" // Fixed date !
175 | > }
176 | > ```
177 | >
178 | > The `GoogleDocs` node for document `Post 2` will have a custom slug
179 | >
180 | > ```js
181 | > {
182 | > path: "/en/posts/post-2"
183 | > slug: "/custom/slug"
184 | > breadcrumb: [
185 | > {name: "Posts", slug: "/posts"},
186 | > {name: "Post 2", slug: "/custom/slug"},
187 | > ],
188 | > template: "special-post", // src/templates/special-post.js
189 | > locale: "en",
190 | > date: "2020-09-12" // Google Drive document creation date
191 | > }
192 | > ```
193 |
194 | You also can add metadata (`locale`, `date`, `template`, ...) to your documents.
195 |
196 |
197 |
198 | ### Add the plugin to your `gatsby-config.js` file
199 |
200 | | Option | Required | Type | Default | Example |
201 | | ---------------- | -------- | ------- | ------- | -------------- |
202 | | folder | `true` | String | `null` | `"1Tn1dCbIc"` |
203 | | createPages | `false` | Boolean | `false` | `true` |
204 | | pageContext | `false` | Array | `[]` | `["locale"]` |
205 | | demoteHeadings | `false` | Boolean | `true` | `false` |
206 | | imagesOptions | `false` | Object | `null` | `{width: 512}` |
207 | | keepDefaultStyle | `false` | Boolean | `false` | `true` |
208 | | skipCodes | `false` | Boolean | `false` | `true` |
209 | | skipFootnotes | `false` | Boolean | `false` | `true` |
210 | | skipHeadings | `false` | Boolean | `false` | `true` |
211 | | skipImages | `false` | Boolean | `false` | `true` |
212 | | skipLists | `false` | Boolean | `false` | `true` |
213 | | skipQuotes | `false` | Boolean | `false` | `true` |
214 | | skipTables | `false` | Boolean | `false` | `true` |
215 | | debug | `false` | Boolean | `false` | `true` |
216 |
217 | ```js
218 | module.exports = {
219 | plugins: [
220 | {
221 | resolve: "gatsby-source-google-docs",
222 | options: {
223 | // https://drive.google.com/drive/folders/FOLDER_ID
224 | folder: "FOLDER_ID",
225 | createPages: true,
226 | },
227 | },
228 | "gatsby-transformer-remark",
229 | //
230 | // OR "gatsby-plugin-mdx" for advanced usage using MDX
231 | //
232 | // You need some transformations?
233 | // Checkout https://www.gatsbyjs.com/plugins/?=gatsby-remark
234 | // And pick-up some plugins
235 | ],
236 | }
237 | ```
238 |
239 | 📷 How to use images ?
240 |
241 | `gatsby-plugin-sharp`, `gatsby-transformer-sharp` and `gatsby-remark-images` are required if you want to take advantage of [gatsby-image blur-up technique](https://using-gatsby-image.gatsbyjs.org/blur-up/).
242 |
243 | ```shell
244 | yarn add gatsby-plugin-sharp gatsby-transformer-sharp gatsby-remark-images
245 | ```
246 |
247 | ```js
248 | module.exports = {
249 | plugins: [
250 | "gatsby-source-google-docs",
251 | "gatsby-plugin-sharp",
252 | "gatsby-transformer-sharp",
253 | {
254 | resolve: "gatsby-transformer-remark",
255 | options: {
256 | plugins: ["gatsby-remark-images"],
257 | },
258 | },
259 | ],
260 | }
261 | ```
262 |
263 |
264 |
265 | ⚛️ How to use codes blocks ?
266 |
267 | Use [Code Blocks](https://gsuite.google.com/marketplace/app/code_blocks/100740430168) Google Docs extension to format your code blocks.
268 |
269 | To specify the lang, you need to add a fist line in your code block following the format `lang:javascript`.
270 |
271 | To get Syntax highlighting, I recommend using `prismjs` but it's not mandatory.
272 |
273 | ```shell
274 | yarn add gatsby-remark-prismjs prismjs
275 | ```
276 |
277 | Add the `gatsby-remark-prismjs` plugin to your `gatsby-config.js`
278 |
279 | ```js
280 | module.exports = {
281 | plugins: [
282 | "gatsby-source-google-docs",
283 | {
284 | resolve: "gatsby-transformer-remark",
285 | options: {
286 | plugins: ["gatsby-remark-prismjs"],
287 | },
288 | },
289 | ],
290 | }
291 | ```
292 |
293 | Import a `prismjs` theme in your `gatsby-browser.js`
294 |
295 | ```js
296 | require("prismjs/themes/prism.css")
297 | ```
298 |
299 |
300 |
301 | ### Create templates and pages
302 |
303 | Using `createPages: true` option, pages will be created automatically.
304 | You need to create templates and define wich template to use using `YAML` metadata.
305 |
306 | > You can set `page: false` metadata for a document to prevent a page creation
307 |
308 | Checkout the [example template](./example/src/templates/page.js) and adapt it to your needs.
309 |
310 | > You can use `pageContext` option if you need extra data into the context of your pages.
311 |
312 | How to create pages manualy?
313 |
314 | If you prefer to create pages manualy, checkout the [createPages API](./src/utils/create-pages.js) et adapt it to your needs.
315 |
316 |
317 |
318 | ### Trigger production builds
319 |
320 | - Go to [Google Drive example folder](https://drive.google.com/drive/folders/1YJWX_FRoVusp-51ztedm6HSZqpbJA3ag)
321 | - Make a copy of **Trigger Gatsby Build** file using `Right Click -> Make a copy`
322 | - Open your copy and update the **Build Webhook URL** in `A2`
323 | - Click the **Deploy** button to trigger a new build
324 |
325 | > This method works with any hosting services: Gatsby Cloud, Netlify...
326 |
327 | ## Showcase
328 |
329 | You are using `gatsby-source-google-docs` for your website? Thank you!
330 | Please add the link bellow:
331 |
332 | - [documentation](https://cedricdelpoux.github.io/gatsby-source-google-docs/)
333 | - [cedricdelpoux](https://cedricdelpoux.fr/en)
334 |
335 | ## Contributing
336 |
337 | - ⇄ Pull/Merge requests and ★ Stars are always welcome.
338 | - For bugs and feature requests, please [create an issue][github-issue].
339 |
340 | [badge-paypal]: https://img.shields.io/badge/sponsor-PayPal-3b7bbf.svg?style=flat-square
341 | [badge-npm]: https://img.shields.io/npm/v/gatsby-source-google-docs.svg?style=flat-square
342 | [badge-downloads]: https://img.shields.io/npm/dt/gatsby-source-google-docs.svg?style=flat-square
343 | [badge-build]: https://img.shields.io/travis/cedricdelpoux/gatsby-source-google-docs/master?style=flat-square
344 | [badge-coverage]: https://img.shields.io/codecov/c/github/cedricdelpoux/gatsby-source-google-docs/master.svg?style=flat-square
345 | [badge-licence]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square
346 | [badge-prs]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
347 | [npm]: https://www.npmjs.org/package/gatsby-source-google-docs
348 | [travis]: https://travis-ci.com/cedricdelpoux/gatsby-source-google-docs
349 | [codecov]: https://codecov.io/gh/cedricdelpoux/gatsby-source-google-docs
350 | [github-issue]: https://github.com/cedricdelpoux/gatsby-source-google-docs/issues/new
351 |
--------------------------------------------------------------------------------
/__tests__/documents/breaks.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Breaks",
3 | "body": {
4 | "content": [
5 | {
6 | "endIndex": 1,
7 | "sectionBreak": {
8 | "sectionStyle": {
9 | "columnSeparatorStyle": "NONE",
10 | "contentDirection": "LEFT_TO_RIGHT",
11 | "sectionType": "CONTINUOUS"
12 | }
13 | }
14 | },
15 | {
16 | "startIndex": 1,
17 | "endIndex": 2,
18 | "paragraph": {
19 | "elements": [
20 | {
21 | "startIndex": 1,
22 | "endIndex": 2,
23 | "textRun": {"content": "\n", "textStyle": {}}
24 | }
25 | ],
26 | "paragraphStyle": {
27 | "namedStyleType": "NORMAL_TEXT",
28 | "direction": "LEFT_TO_RIGHT"
29 | }
30 | }
31 | },
32 | {
33 | "startIndex": 2,
34 | "endIndex": 3,
35 | "sectionBreak": {
36 | "sectionStyle": {
37 | "columnSeparatorStyle": "NONE",
38 | "contentDirection": "LEFT_TO_RIGHT",
39 | "sectionType": "NEXT_PAGE"
40 | }
41 | }
42 | },
43 | {
44 | "startIndex": 3,
45 | "endIndex": 5,
46 | "paragraph": {
47 | "elements": [
48 | {"startIndex": 3, "endIndex": 4, "pageBreak": {"textStyle": {}}},
49 | {
50 | "startIndex": 4,
51 | "endIndex": 5,
52 | "textRun": {"content": "\n", "textStyle": {}}
53 | }
54 | ],
55 | "paragraphStyle": {
56 | "namedStyleType": "NORMAL_TEXT",
57 | "direction": "LEFT_TO_RIGHT"
58 | }
59 | }
60 | },
61 | {
62 | "startIndex": 5,
63 | "endIndex": 6,
64 | "paragraph": {
65 | "elements": [
66 | {
67 | "startIndex": 5,
68 | "endIndex": 6,
69 | "textRun": {"content": "\n", "textStyle": {}}
70 | }
71 | ],
72 | "paragraphStyle": {
73 | "namedStyleType": "NORMAL_TEXT",
74 | "direction": "LEFT_TO_RIGHT"
75 | }
76 | }
77 | },
78 | {
79 | "startIndex": 6,
80 | "endIndex": 7,
81 | "sectionBreak": {
82 | "sectionStyle": {
83 | "columnSeparatorStyle": "NONE",
84 | "contentDirection": "LEFT_TO_RIGHT",
85 | "sectionType": "CONTINUOUS"
86 | }
87 | }
88 | },
89 | {
90 | "startIndex": 7,
91 | "endIndex": 8,
92 | "paragraph": {
93 | "elements": [
94 | {
95 | "startIndex": 7,
96 | "endIndex": 8,
97 | "textRun": {"content": "\n", "textStyle": {}}
98 | }
99 | ],
100 | "paragraphStyle": {
101 | "namedStyleType": "NORMAL_TEXT",
102 | "direction": "LEFT_TO_RIGHT"
103 | }
104 | }
105 | }
106 | ]
107 | },
108 | "documentStyle": {
109 | "background": {"color": {}},
110 | "pageNumberStart": 1,
111 | "marginTop": {"magnitude": 72, "unit": "PT"},
112 | "marginBottom": {"magnitude": 72, "unit": "PT"},
113 | "marginRight": {"magnitude": 72, "unit": "PT"},
114 | "marginLeft": {"magnitude": 72, "unit": "PT"},
115 | "pageSize": {
116 | "height": {"magnitude": 841.8897637795277, "unit": "PT"},
117 | "width": {"magnitude": 595.2755905511812, "unit": "PT"}
118 | },
119 | "marginHeader": {"magnitude": 36, "unit": "PT"},
120 | "marginFooter": {"magnitude": 36, "unit": "PT"},
121 | "useCustomHeaderFooterMargins": true
122 | },
123 | "namedStyles": {
124 | "styles": [
125 | {
126 | "namedStyleType": "NORMAL_TEXT",
127 | "textStyle": {
128 | "bold": false,
129 | "italic": false,
130 | "underline": false,
131 | "strikethrough": false,
132 | "smallCaps": false,
133 | "backgroundColor": {},
134 | "foregroundColor": {"color": {"rgbColor": {}}},
135 | "fontSize": {"magnitude": 11, "unit": "PT"},
136 | "weightedFontFamily": {"fontFamily": "Arial", "weight": 400},
137 | "baselineOffset": "NONE"
138 | },
139 | "paragraphStyle": {
140 | "namedStyleType": "NORMAL_TEXT",
141 | "alignment": "START",
142 | "lineSpacing": 115,
143 | "direction": "LEFT_TO_RIGHT",
144 | "spacingMode": "COLLAPSE_LISTS",
145 | "spaceAbove": {"unit": "PT"},
146 | "spaceBelow": {"unit": "PT"},
147 | "borderBetween": {
148 | "color": {},
149 | "width": {"unit": "PT"},
150 | "padding": {"unit": "PT"},
151 | "dashStyle": "SOLID"
152 | },
153 | "borderTop": {
154 | "color": {},
155 | "width": {"unit": "PT"},
156 | "padding": {"unit": "PT"},
157 | "dashStyle": "SOLID"
158 | },
159 | "borderBottom": {
160 | "color": {},
161 | "width": {"unit": "PT"},
162 | "padding": {"unit": "PT"},
163 | "dashStyle": "SOLID"
164 | },
165 | "borderLeft": {
166 | "color": {},
167 | "width": {"unit": "PT"},
168 | "padding": {"unit": "PT"},
169 | "dashStyle": "SOLID"
170 | },
171 | "borderRight": {
172 | "color": {},
173 | "width": {"unit": "PT"},
174 | "padding": {"unit": "PT"},
175 | "dashStyle": "SOLID"
176 | },
177 | "indentFirstLine": {"unit": "PT"},
178 | "indentStart": {"unit": "PT"},
179 | "indentEnd": {"unit": "PT"},
180 | "keepLinesTogether": false,
181 | "keepWithNext": false,
182 | "avoidWidowAndOrphan": false,
183 | "shading": {"backgroundColor": {}}
184 | }
185 | },
186 | {
187 | "namedStyleType": "HEADING_1",
188 | "textStyle": {
189 | "bold": true,
190 | "foregroundColor": {
191 | "color": {
192 | "rgbColor": {
193 | "red": 0.10980392,
194 | "green": 0.27058825,
195 | "blue": 0.5294118
196 | }
197 | }
198 | },
199 | "fontSize": {"magnitude": 16, "unit": "PT"},
200 | "weightedFontFamily": {"fontFamily": "Trebuchet MS", "weight": 400}
201 | },
202 | "paragraphStyle": {
203 | "headingId": "h.4u3anmqa57uf",
204 | "namedStyleType": "HEADING_1",
205 | "direction": "LEFT_TO_RIGHT",
206 | "spacingMode": "COLLAPSE_LISTS",
207 | "spaceAbove": {"magnitude": 10, "unit": "PT"},
208 | "indentFirstLine": {"magnitude": 36, "unit": "PT"},
209 | "indentStart": {"magnitude": 36, "unit": "PT"},
210 | "keepLinesTogether": false,
211 | "keepWithNext": false,
212 | "avoidWidowAndOrphan": false
213 | }
214 | },
215 | {
216 | "namedStyleType": "HEADING_2",
217 | "textStyle": {
218 | "bold": true,
219 | "foregroundColor": {
220 | "color": {
221 | "rgbColor": {
222 | "red": 0.23921569,
223 | "green": 0.52156866,
224 | "blue": 0.7764706
225 | }
226 | }
227 | },
228 | "fontSize": {"magnitude": 13, "unit": "PT"},
229 | "weightedFontFamily": {"fontFamily": "Trebuchet MS", "weight": 400}
230 | },
231 | "paragraphStyle": {
232 | "headingId": "h.yh37ranpban7",
233 | "namedStyleType": "HEADING_2",
234 | "direction": "LEFT_TO_RIGHT",
235 | "spacingMode": "COLLAPSE_LISTS",
236 | "spaceAbove": {"magnitude": 10, "unit": "PT"},
237 | "indentFirstLine": {"magnitude": 72, "unit": "PT"},
238 | "indentStart": {"magnitude": 72, "unit": "PT"},
239 | "keepLinesTogether": false,
240 | "keepWithNext": false,
241 | "avoidWidowAndOrphan": false
242 | }
243 | },
244 | {
245 | "namedStyleType": "HEADING_3",
246 | "textStyle": {
247 | "bold": true,
248 | "foregroundColor": {
249 | "color": {
250 | "rgbColor": {
251 | "red": 0.42745098,
252 | "green": 0.61960787,
253 | "blue": 0.92156863
254 | }
255 | }
256 | },
257 | "fontSize": {"magnitude": 12, "unit": "PT"},
258 | "weightedFontFamily": {"fontFamily": "Trebuchet MS", "weight": 400}
259 | },
260 | "paragraphStyle": {
261 | "headingId": "h.x2p4yxmolykq",
262 | "namedStyleType": "HEADING_3",
263 | "direction": "LEFT_TO_RIGHT",
264 | "spacingMode": "COLLAPSE_LISTS",
265 | "spaceAbove": {"magnitude": 8, "unit": "PT"},
266 | "indentFirstLine": {"magnitude": 36, "unit": "PT"},
267 | "indentStart": {"magnitude": 36, "unit": "PT"},
268 | "keepLinesTogether": false,
269 | "keepWithNext": false,
270 | "avoidWidowAndOrphan": false
271 | }
272 | },
273 | {
274 | "namedStyleType": "HEADING_4",
275 | "textStyle": {
276 | "underline": true,
277 | "foregroundColor": {
278 | "color": {"rgbColor": {"red": 0.4, "green": 0.4, "blue": 0.4}}
279 | },
280 | "fontSize": {"magnitude": 11, "unit": "PT"},
281 | "weightedFontFamily": {"fontFamily": "Trebuchet MS", "weight": 400}
282 | },
283 | "paragraphStyle": {
284 | "namedStyleType": "NORMAL_TEXT",
285 | "direction": "LEFT_TO_RIGHT",
286 | "spacingMode": "COLLAPSE_LISTS",
287 | "spaceAbove": {"magnitude": 8, "unit": "PT"},
288 | "spaceBelow": {"unit": "PT"},
289 | "keepLinesTogether": false,
290 | "keepWithNext": false,
291 | "avoidWidowAndOrphan": false
292 | }
293 | },
294 | {
295 | "namedStyleType": "HEADING_5",
296 | "textStyle": {
297 | "foregroundColor": {
298 | "color": {"rgbColor": {"red": 0.4, "green": 0.4, "blue": 0.4}}
299 | },
300 | "fontSize": {"magnitude": 11, "unit": "PT"},
301 | "weightedFontFamily": {"fontFamily": "Trebuchet MS", "weight": 400}
302 | },
303 | "paragraphStyle": {
304 | "namedStyleType": "NORMAL_TEXT",
305 | "direction": "LEFT_TO_RIGHT",
306 | "spacingMode": "COLLAPSE_LISTS",
307 | "spaceAbove": {"magnitude": 8, "unit": "PT"},
308 | "spaceBelow": {"unit": "PT"},
309 | "keepLinesTogether": false,
310 | "keepWithNext": false,
311 | "avoidWidowAndOrphan": false
312 | }
313 | },
314 | {
315 | "namedStyleType": "HEADING_6",
316 | "textStyle": {
317 | "italic": true,
318 | "foregroundColor": {
319 | "color": {"rgbColor": {"red": 0.4, "green": 0.4, "blue": 0.4}}
320 | },
321 | "fontSize": {"magnitude": 11, "unit": "PT"},
322 | "weightedFontFamily": {"fontFamily": "Trebuchet MS", "weight": 400}
323 | },
324 | "paragraphStyle": {
325 | "namedStyleType": "NORMAL_TEXT",
326 | "direction": "LEFT_TO_RIGHT",
327 | "spacingMode": "COLLAPSE_LISTS",
328 | "spaceAbove": {"magnitude": 8, "unit": "PT"},
329 | "spaceBelow": {"unit": "PT"},
330 | "keepLinesTogether": false,
331 | "keepWithNext": false,
332 | "avoidWidowAndOrphan": false
333 | }
334 | },
335 | {
336 | "namedStyleType": "TITLE",
337 | "textStyle": {
338 | "fontSize": {"magnitude": 21, "unit": "PT"},
339 | "weightedFontFamily": {"fontFamily": "Trebuchet MS", "weight": 400}
340 | },
341 | "paragraphStyle": {
342 | "namedStyleType": "NORMAL_TEXT",
343 | "direction": "LEFT_TO_RIGHT",
344 | "spacingMode": "COLLAPSE_LISTS",
345 | "spaceAbove": {"unit": "PT"},
346 | "spaceBelow": {"unit": "PT"},
347 | "keepLinesTogether": false,
348 | "keepWithNext": false,
349 | "avoidWidowAndOrphan": false
350 | }
351 | },
352 | {
353 | "namedStyleType": "SUBTITLE",
354 | "textStyle": {
355 | "italic": true,
356 | "foregroundColor": {
357 | "color": {"rgbColor": {"red": 0.4, "green": 0.4, "blue": 0.4}}
358 | },
359 | "fontSize": {"magnitude": 13, "unit": "PT"},
360 | "weightedFontFamily": {"fontFamily": "Trebuchet MS", "weight": 400}
361 | },
362 | "paragraphStyle": {
363 | "namedStyleType": "NORMAL_TEXT",
364 | "direction": "LEFT_TO_RIGHT",
365 | "spacingMode": "COLLAPSE_LISTS",
366 | "spaceAbove": {"unit": "PT"},
367 | "spaceBelow": {"magnitude": 10, "unit": "PT"},
368 | "keepLinesTogether": false,
369 | "keepWithNext": false,
370 | "avoidWidowAndOrphan": false
371 | }
372 | }
373 | ]
374 | },
375 | "revisionId": "ALm37BVFUi1tyP2saQFTvPhPpjrX4Nl9gkb5T_6RSqjSO0h-lyjBNv3KUjtPaipCScVs7h4ZyWmAdjyHaQTtmA",
376 | "suggestionsViewMode": "SUGGESTIONS_INLINE",
377 | "documentId": "19VJJx0vGGNm0x-GmG4tw0s1Tk0Ezxyx-elORaNeGZLs"
378 | }
379 |
--------------------------------------------------------------------------------
/__tests__/documents/french.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "French",
3 | "body": {
4 | "content": [
5 | {
6 | "endIndex": 1,
7 | "sectionBreak": {
8 | "sectionStyle": {
9 | "columnSeparatorStyle": "NONE",
10 | "contentDirection": "LEFT_TO_RIGHT",
11 | "sectionType": "CONTINUOUS"
12 | }
13 | }
14 | },
15 | {
16 | "startIndex": 1,
17 | "endIndex": 56,
18 | "paragraph": {
19 | "elements": [
20 | {
21 | "startIndex": 1,
22 | "endIndex": 14,
23 | "textRun": {"content": "Je m'appelle ", "textStyle": {}}
24 | },
25 | {
26 | "startIndex": 14,
27 | "endIndex": 20,
28 | "textRun": {"content": "Cédric", "textStyle": {"bold": true}}
29 | },
30 | {
31 | "startIndex": 20,
32 | "endIndex": 42,
33 | "textRun": {"content": ", j'habite à Toulouse ", "textStyle": {}}
34 | },
35 | {
36 | "startIndex": 42,
37 | "endIndex": 50,
38 | "textRun": {"content": "(France)", "textStyle": {"italic": true}}
39 | },
40 | {
41 | "startIndex": 50,
42 | "endIndex": 56,
43 | "textRun": {"content": " 🇫🇷\n", "textStyle": {}}
44 | }
45 | ],
46 | "paragraphStyle": {
47 | "namedStyleType": "NORMAL_TEXT",
48 | "direction": "LEFT_TO_RIGHT"
49 | }
50 | }
51 | }
52 | ]
53 | },
54 | "headers": {
55 | "kix.1ahtg9vtgmel": {
56 | "headerId": "kix.1ahtg9vtgmel",
57 | "content": [
58 | {
59 | "endIndex": 1,
60 | "paragraph": {
61 | "elements": [
62 | {"endIndex": 1, "textRun": {"content": "\n", "textStyle": {}}}
63 | ],
64 | "paragraphStyle": {
65 | "namedStyleType": "NORMAL_TEXT",
66 | "direction": "LEFT_TO_RIGHT"
67 | }
68 | }
69 | }
70 | ]
71 | },
72 | "kix.tb1914qxojd3": {
73 | "headerId": "kix.tb1914qxojd3",
74 | "content": [
75 | {
76 | "endIndex": 1,
77 | "paragraph": {
78 | "elements": [
79 | {"endIndex": 1, "textRun": {"content": "\n", "textStyle": {}}}
80 | ],
81 | "paragraphStyle": {
82 | "headingId": "h.c2m8jqfxedns",
83 | "namedStyleType": "HEADING_1",
84 | "direction": "LEFT_TO_RIGHT"
85 | }
86 | }
87 | }
88 | ]
89 | }
90 | },
91 | "footers": {
92 | "kix.j1vlyiu8ewv2": {
93 | "footerId": "kix.j1vlyiu8ewv2",
94 | "content": [
95 | {
96 | "endIndex": 1,
97 | "paragraph": {
98 | "elements": [
99 | {"endIndex": 1, "textRun": {"content": "\n", "textStyle": {}}}
100 | ],
101 | "paragraphStyle": {
102 | "namedStyleType": "NORMAL_TEXT",
103 | "direction": "LEFT_TO_RIGHT"
104 | }
105 | }
106 | }
107 | ]
108 | }
109 | },
110 | "documentStyle": {
111 | "background": {"color": {}},
112 | "defaultHeaderId": "kix.1ahtg9vtgmel",
113 | "firstPageHeaderId": "kix.tb1914qxojd3",
114 | "firstPageFooterId": "kix.j1vlyiu8ewv2",
115 | "useFirstPageHeaderFooter": true,
116 | "pageNumberStart": 1,
117 | "marginTop": {"magnitude": 72, "unit": "PT"},
118 | "marginBottom": {"magnitude": 72, "unit": "PT"},
119 | "marginRight": {"magnitude": 72, "unit": "PT"},
120 | "marginLeft": {"magnitude": 72, "unit": "PT"},
121 | "pageSize": {
122 | "height": {"magnitude": 841.8897637795277, "unit": "PT"},
123 | "width": {"magnitude": 595.2755905511812, "unit": "PT"}
124 | },
125 | "marginHeader": {"magnitude": 36, "unit": "PT"},
126 | "marginFooter": {"magnitude": 36, "unit": "PT"},
127 | "useCustomHeaderFooterMargins": true
128 | },
129 | "namedStyles": {
130 | "styles": [
131 | {
132 | "namedStyleType": "NORMAL_TEXT",
133 | "textStyle": {
134 | "bold": false,
135 | "italic": false,
136 | "underline": false,
137 | "strikethrough": false,
138 | "smallCaps": false,
139 | "backgroundColor": {},
140 | "foregroundColor": {"color": {"rgbColor": {}}},
141 | "fontSize": {"magnitude": 11, "unit": "PT"},
142 | "weightedFontFamily": {"fontFamily": "Arial", "weight": 400},
143 | "baselineOffset": "NONE"
144 | },
145 | "paragraphStyle": {
146 | "namedStyleType": "NORMAL_TEXT",
147 | "alignment": "START",
148 | "lineSpacing": 115,
149 | "direction": "LEFT_TO_RIGHT",
150 | "spacingMode": "COLLAPSE_LISTS",
151 | "spaceAbove": {"unit": "PT"},
152 | "spaceBelow": {"unit": "PT"},
153 | "borderBetween": {
154 | "color": {},
155 | "width": {"unit": "PT"},
156 | "padding": {"unit": "PT"},
157 | "dashStyle": "SOLID"
158 | },
159 | "borderTop": {
160 | "color": {},
161 | "width": {"unit": "PT"},
162 | "padding": {"unit": "PT"},
163 | "dashStyle": "SOLID"
164 | },
165 | "borderBottom": {
166 | "color": {},
167 | "width": {"unit": "PT"},
168 | "padding": {"unit": "PT"},
169 | "dashStyle": "SOLID"
170 | },
171 | "borderLeft": {
172 | "color": {},
173 | "width": {"unit": "PT"},
174 | "padding": {"unit": "PT"},
175 | "dashStyle": "SOLID"
176 | },
177 | "borderRight": {
178 | "color": {},
179 | "width": {"unit": "PT"},
180 | "padding": {"unit": "PT"},
181 | "dashStyle": "SOLID"
182 | },
183 | "indentFirstLine": {"unit": "PT"},
184 | "indentStart": {"unit": "PT"},
185 | "indentEnd": {"unit": "PT"},
186 | "keepLinesTogether": false,
187 | "keepWithNext": false,
188 | "avoidWidowAndOrphan": true,
189 | "shading": {"backgroundColor": {}}
190 | }
191 | },
192 | {
193 | "namedStyleType": "HEADING_1",
194 | "textStyle": {"fontSize": {"magnitude": 20, "unit": "PT"}},
195 | "paragraphStyle": {
196 | "namedStyleType": "NORMAL_TEXT",
197 | "direction": "LEFT_TO_RIGHT",
198 | "spaceAbove": {"magnitude": 20, "unit": "PT"},
199 | "spaceBelow": {"magnitude": 6, "unit": "PT"},
200 | "keepLinesTogether": true,
201 | "keepWithNext": true
202 | }
203 | },
204 | {
205 | "namedStyleType": "HEADING_2",
206 | "textStyle": {
207 | "bold": false,
208 | "fontSize": {"magnitude": 16, "unit": "PT"}
209 | },
210 | "paragraphStyle": {
211 | "namedStyleType": "NORMAL_TEXT",
212 | "direction": "LEFT_TO_RIGHT",
213 | "spaceAbove": {"magnitude": 18, "unit": "PT"},
214 | "spaceBelow": {"magnitude": 6, "unit": "PT"},
215 | "keepLinesTogether": true,
216 | "keepWithNext": true
217 | }
218 | },
219 | {
220 | "namedStyleType": "HEADING_3",
221 | "textStyle": {
222 | "bold": false,
223 | "foregroundColor": {
224 | "color": {
225 | "rgbColor": {
226 | "red": 0.2627451,
227 | "green": 0.2627451,
228 | "blue": 0.2627451
229 | }
230 | }
231 | },
232 | "fontSize": {"magnitude": 14, "unit": "PT"}
233 | },
234 | "paragraphStyle": {
235 | "namedStyleType": "NORMAL_TEXT",
236 | "direction": "LEFT_TO_RIGHT",
237 | "spaceAbove": {"magnitude": 16, "unit": "PT"},
238 | "spaceBelow": {"magnitude": 4, "unit": "PT"},
239 | "keepLinesTogether": true,
240 | "keepWithNext": true
241 | }
242 | },
243 | {
244 | "namedStyleType": "HEADING_4",
245 | "textStyle": {
246 | "foregroundColor": {
247 | "color": {"rgbColor": {"red": 0.4, "green": 0.4, "blue": 0.4}}
248 | },
249 | "fontSize": {"magnitude": 12, "unit": "PT"}
250 | },
251 | "paragraphStyle": {
252 | "namedStyleType": "NORMAL_TEXT",
253 | "direction": "LEFT_TO_RIGHT",
254 | "spaceAbove": {"magnitude": 14, "unit": "PT"},
255 | "spaceBelow": {"magnitude": 4, "unit": "PT"},
256 | "keepLinesTogether": true,
257 | "keepWithNext": true
258 | }
259 | },
260 | {
261 | "namedStyleType": "HEADING_5",
262 | "textStyle": {
263 | "foregroundColor": {
264 | "color": {"rgbColor": {"red": 0.4, "green": 0.4, "blue": 0.4}}
265 | },
266 | "fontSize": {"magnitude": 11, "unit": "PT"}
267 | },
268 | "paragraphStyle": {
269 | "namedStyleType": "NORMAL_TEXT",
270 | "direction": "LEFT_TO_RIGHT",
271 | "spaceAbove": {"magnitude": 12, "unit": "PT"},
272 | "spaceBelow": {"magnitude": 4, "unit": "PT"},
273 | "keepLinesTogether": true,
274 | "keepWithNext": true
275 | }
276 | },
277 | {
278 | "namedStyleType": "HEADING_6",
279 | "textStyle": {
280 | "italic": true,
281 | "foregroundColor": {
282 | "color": {"rgbColor": {"red": 0.4, "green": 0.4, "blue": 0.4}}
283 | },
284 | "fontSize": {"magnitude": 11, "unit": "PT"}
285 | },
286 | "paragraphStyle": {
287 | "namedStyleType": "NORMAL_TEXT",
288 | "direction": "LEFT_TO_RIGHT",
289 | "spaceAbove": {"magnitude": 12, "unit": "PT"},
290 | "spaceBelow": {"magnitude": 4, "unit": "PT"},
291 | "keepLinesTogether": true,
292 | "keepWithNext": true
293 | }
294 | },
295 | {
296 | "namedStyleType": "TITLE",
297 | "textStyle": {"fontSize": {"magnitude": 26, "unit": "PT"}},
298 | "paragraphStyle": {
299 | "namedStyleType": "NORMAL_TEXT",
300 | "direction": "LEFT_TO_RIGHT",
301 | "spaceAbove": {"unit": "PT"},
302 | "spaceBelow": {"magnitude": 3, "unit": "PT"},
303 | "keepLinesTogether": true,
304 | "keepWithNext": true
305 | }
306 | },
307 | {
308 | "namedStyleType": "SUBTITLE",
309 | "textStyle": {
310 | "italic": false,
311 | "foregroundColor": {
312 | "color": {"rgbColor": {"red": 0.4, "green": 0.4, "blue": 0.4}}
313 | },
314 | "fontSize": {"magnitude": 15, "unit": "PT"},
315 | "weightedFontFamily": {"fontFamily": "Arial", "weight": 400}
316 | },
317 | "paragraphStyle": {
318 | "namedStyleType": "NORMAL_TEXT",
319 | "direction": "LEFT_TO_RIGHT",
320 | "spaceAbove": {"unit": "PT"},
321 | "spaceBelow": {"magnitude": 16, "unit": "PT"},
322 | "keepLinesTogether": true,
323 | "keepWithNext": true
324 | }
325 | }
326 | ]
327 | },
328 | "lists": {
329 | "kix.2ndctxh20b9f": {
330 | "listProperties": {
331 | "nestingLevels": [
332 | {
333 | "bulletAlignment": "START",
334 | "glyphSymbol": "●",
335 | "glyphFormat": "%0",
336 | "indentFirstLine": {"magnitude": 18, "unit": "PT"},
337 | "indentStart": {"magnitude": 36, "unit": "PT"},
338 | "textStyle": {"underline": false},
339 | "startNumber": 1
340 | },
341 | {
342 | "bulletAlignment": "START",
343 | "glyphSymbol": "○",
344 | "glyphFormat": "%1",
345 | "indentFirstLine": {"magnitude": 54, "unit": "PT"},
346 | "indentStart": {"magnitude": 72, "unit": "PT"},
347 | "textStyle": {"underline": false},
348 | "startNumber": 1
349 | },
350 | {
351 | "bulletAlignment": "START",
352 | "glyphSymbol": "■",
353 | "glyphFormat": "%2",
354 | "indentFirstLine": {"magnitude": 90, "unit": "PT"},
355 | "indentStart": {"magnitude": 108, "unit": "PT"},
356 | "textStyle": {"underline": false},
357 | "startNumber": 1
358 | },
359 | {
360 | "bulletAlignment": "START",
361 | "glyphSymbol": "●",
362 | "glyphFormat": "%3",
363 | "indentFirstLine": {"magnitude": 126, "unit": "PT"},
364 | "indentStart": {"magnitude": 144, "unit": "PT"},
365 | "textStyle": {"underline": false},
366 | "startNumber": 1
367 | },
368 | {
369 | "bulletAlignment": "START",
370 | "glyphSymbol": "○",
371 | "glyphFormat": "%4",
372 | "indentFirstLine": {"magnitude": 162, "unit": "PT"},
373 | "indentStart": {"magnitude": 180, "unit": "PT"},
374 | "textStyle": {"underline": false},
375 | "startNumber": 1
376 | },
377 | {
378 | "bulletAlignment": "START",
379 | "glyphSymbol": "■",
380 | "glyphFormat": "%5",
381 | "indentFirstLine": {"magnitude": 198, "unit": "PT"},
382 | "indentStart": {"magnitude": 216, "unit": "PT"},
383 | "textStyle": {"underline": false},
384 | "startNumber": 1
385 | },
386 | {
387 | "bulletAlignment": "START",
388 | "glyphSymbol": "●",
389 | "glyphFormat": "%6",
390 | "indentFirstLine": {"magnitude": 234, "unit": "PT"},
391 | "indentStart": {"magnitude": 252, "unit": "PT"},
392 | "textStyle": {"underline": false},
393 | "startNumber": 1
394 | },
395 | {
396 | "bulletAlignment": "START",
397 | "glyphSymbol": "○",
398 | "glyphFormat": "%7",
399 | "indentFirstLine": {"magnitude": 270, "unit": "PT"},
400 | "indentStart": {"magnitude": 288, "unit": "PT"},
401 | "textStyle": {"underline": false},
402 | "startNumber": 1
403 | },
404 | {
405 | "bulletAlignment": "START",
406 | "glyphSymbol": "■",
407 | "glyphFormat": "%8",
408 | "indentFirstLine": {"magnitude": 306, "unit": "PT"},
409 | "indentStart": {"magnitude": 324, "unit": "PT"},
410 | "textStyle": {"underline": false},
411 | "startNumber": 1
412 | }
413 | ]
414 | }
415 | }
416 | },
417 | "revisionId": "ALm37BUJaO2mOgrManmcUvfm44nAAFpzJ9C6dHTAUcCcppdy2yg_uiU8Ugtj5aEmvN3DlAcrWfEIgkgQ-nFedg",
418 | "suggestionsViewMode": "SUGGESTIONS_INLINE",
419 | "documentId": "1BM_DE77JGmhxa4XrXXaDYuAjQGVjf3MCupETAgbUCQg"
420 | }
421 |
--------------------------------------------------------------------------------
/__tests__/documents/poem.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Poem",
3 | "body": {
4 | "content": [
5 | {
6 | "endIndex": 1,
7 | "sectionBreak": {
8 | "sectionStyle": {
9 | "columnSeparatorStyle": "NONE",
10 | "contentDirection": "LEFT_TO_RIGHT",
11 | "sectionType": "CONTINUOUS"
12 | }
13 | }
14 | },
15 | {
16 | "startIndex": 1,
17 | "endIndex": 10,
18 | "paragraph": {
19 | "elements": [
20 | {
21 | "startIndex": 1,
22 | "endIndex": 10,
23 | "textRun": {"content": "The poem\n", "textStyle": {}}
24 | }
25 | ],
26 | "paragraphStyle": {
27 | "namedStyleType": "NORMAL_TEXT",
28 | "direction": "LEFT_TO_RIGHT"
29 | }
30 | }
31 | },
32 | {
33 | "startIndex": 10,
34 | "endIndex": 34,
35 | "paragraph": {
36 | "elements": [
37 | {
38 | "startIndex": 10,
39 | "endIndex": 34,
40 | "textRun": {
41 | "content": " may have\n",
42 | "textStyle": {}
43 | }
44 | }
45 | ],
46 | "paragraphStyle": {
47 | "namedStyleType": "NORMAL_TEXT",
48 | "direction": "LEFT_TO_RIGHT"
49 | }
50 | }
51 | },
52 | {
53 | "startIndex": 34,
54 | "endIndex": 67,
55 | "paragraph": {
56 | "elements": [
57 | {
58 | "startIndex": 34,
59 | "endIndex": 67,
60 | "textRun": {
61 | "content": " strange whitespace\n",
62 | "textStyle": {}
63 | }
64 | }
65 | ],
66 | "paragraphStyle": {
67 | "namedStyleType": "NORMAL_TEXT",
68 | "direction": "LEFT_TO_RIGHT"
69 | }
70 | }
71 | },
72 | {
73 | "startIndex": 67,
74 | "endIndex": 77,
75 | "paragraph": {
76 | "elements": [
77 | {
78 | "startIndex": 67,
79 | "endIndex": 77,
80 | "textRun": {"content": "patterns.\n", "textStyle": {}}
81 | }
82 | ],
83 | "paragraphStyle": {
84 | "namedStyleType": "NORMAL_TEXT",
85 | "direction": "LEFT_TO_RIGHT"
86 | }
87 | }
88 | }
89 | ]
90 | },
91 | "headers": {
92 | "kix.1ahtg9vtgmel": {
93 | "headerId": "kix.1ahtg9vtgmel",
94 | "content": [
95 | {
96 | "endIndex": 1,
97 | "paragraph": {
98 | "elements": [
99 | {"endIndex": 1, "textRun": {"content": "\n", "textStyle": {}}}
100 | ],
101 | "paragraphStyle": {
102 | "namedStyleType": "NORMAL_TEXT",
103 | "direction": "LEFT_TO_RIGHT"
104 | }
105 | }
106 | }
107 | ]
108 | },
109 | "kix.tb1914qxojd3": {
110 | "headerId": "kix.tb1914qxojd3",
111 | "content": [
112 | {
113 | "endIndex": 1,
114 | "paragraph": {
115 | "elements": [
116 | {"endIndex": 1, "textRun": {"content": "\n", "textStyle": {}}}
117 | ],
118 | "paragraphStyle": {
119 | "headingId": "h.c2m8jqfxedns",
120 | "namedStyleType": "HEADING_1",
121 | "direction": "LEFT_TO_RIGHT"
122 | }
123 | }
124 | }
125 | ]
126 | }
127 | },
128 | "footers": {
129 | "kix.j1vlyiu8ewv2": {
130 | "footerId": "kix.j1vlyiu8ewv2",
131 | "content": [
132 | {
133 | "endIndex": 1,
134 | "paragraph": {
135 | "elements": [
136 | {"endIndex": 1, "textRun": {"content": "\n", "textStyle": {}}}
137 | ],
138 | "paragraphStyle": {
139 | "namedStyleType": "NORMAL_TEXT",
140 | "direction": "LEFT_TO_RIGHT"
141 | }
142 | }
143 | }
144 | ]
145 | }
146 | },
147 | "documentStyle": {
148 | "background": {"color": {}},
149 | "defaultHeaderId": "kix.1ahtg9vtgmel",
150 | "firstPageHeaderId": "kix.tb1914qxojd3",
151 | "firstPageFooterId": "kix.j1vlyiu8ewv2",
152 | "useFirstPageHeaderFooter": true,
153 | "pageNumberStart": 1,
154 | "marginTop": {"magnitude": 72, "unit": "PT"},
155 | "marginBottom": {"magnitude": 72, "unit": "PT"},
156 | "marginRight": {"magnitude": 72, "unit": "PT"},
157 | "marginLeft": {"magnitude": 72, "unit": "PT"},
158 | "pageSize": {
159 | "height": {"magnitude": 841.8897637795277, "unit": "PT"},
160 | "width": {"magnitude": 595.2755905511812, "unit": "PT"}
161 | },
162 | "marginHeader": {"magnitude": 36, "unit": "PT"},
163 | "marginFooter": {"magnitude": 36, "unit": "PT"},
164 | "useCustomHeaderFooterMargins": true
165 | },
166 | "namedStyles": {
167 | "styles": [
168 | {
169 | "namedStyleType": "NORMAL_TEXT",
170 | "textStyle": {
171 | "bold": false,
172 | "italic": false,
173 | "underline": false,
174 | "strikethrough": false,
175 | "smallCaps": false,
176 | "backgroundColor": {},
177 | "foregroundColor": {"color": {"rgbColor": {}}},
178 | "fontSize": {"magnitude": 11, "unit": "PT"},
179 | "weightedFontFamily": {"fontFamily": "Arial", "weight": 400},
180 | "baselineOffset": "NONE"
181 | },
182 | "paragraphStyle": {
183 | "namedStyleType": "NORMAL_TEXT",
184 | "alignment": "START",
185 | "lineSpacing": 115,
186 | "direction": "LEFT_TO_RIGHT",
187 | "spacingMode": "COLLAPSE_LISTS",
188 | "spaceAbove": {"unit": "PT"},
189 | "spaceBelow": {"unit": "PT"},
190 | "borderBetween": {
191 | "color": {},
192 | "width": {"unit": "PT"},
193 | "padding": {"unit": "PT"},
194 | "dashStyle": "SOLID"
195 | },
196 | "borderTop": {
197 | "color": {},
198 | "width": {"unit": "PT"},
199 | "padding": {"unit": "PT"},
200 | "dashStyle": "SOLID"
201 | },
202 | "borderBottom": {
203 | "color": {},
204 | "width": {"unit": "PT"},
205 | "padding": {"unit": "PT"},
206 | "dashStyle": "SOLID"
207 | },
208 | "borderLeft": {
209 | "color": {},
210 | "width": {"unit": "PT"},
211 | "padding": {"unit": "PT"},
212 | "dashStyle": "SOLID"
213 | },
214 | "borderRight": {
215 | "color": {},
216 | "width": {"unit": "PT"},
217 | "padding": {"unit": "PT"},
218 | "dashStyle": "SOLID"
219 | },
220 | "indentFirstLine": {"unit": "PT"},
221 | "indentStart": {"unit": "PT"},
222 | "indentEnd": {"unit": "PT"},
223 | "keepLinesTogether": false,
224 | "keepWithNext": false,
225 | "avoidWidowAndOrphan": true,
226 | "shading": {"backgroundColor": {}}
227 | }
228 | },
229 | {
230 | "namedStyleType": "HEADING_1",
231 | "textStyle": {"fontSize": {"magnitude": 20, "unit": "PT"}},
232 | "paragraphStyle": {
233 | "namedStyleType": "NORMAL_TEXT",
234 | "direction": "LEFT_TO_RIGHT",
235 | "spaceAbove": {"magnitude": 20, "unit": "PT"},
236 | "spaceBelow": {"magnitude": 6, "unit": "PT"},
237 | "keepLinesTogether": true,
238 | "keepWithNext": true
239 | }
240 | },
241 | {
242 | "namedStyleType": "HEADING_2",
243 | "textStyle": {
244 | "bold": false,
245 | "fontSize": {"magnitude": 16, "unit": "PT"}
246 | },
247 | "paragraphStyle": {
248 | "namedStyleType": "NORMAL_TEXT",
249 | "direction": "LEFT_TO_RIGHT",
250 | "spaceAbove": {"magnitude": 18, "unit": "PT"},
251 | "spaceBelow": {"magnitude": 6, "unit": "PT"},
252 | "keepLinesTogether": true,
253 | "keepWithNext": true
254 | }
255 | },
256 | {
257 | "namedStyleType": "HEADING_3",
258 | "textStyle": {
259 | "bold": false,
260 | "foregroundColor": {
261 | "color": {
262 | "rgbColor": {
263 | "red": 0.2627451,
264 | "green": 0.2627451,
265 | "blue": 0.2627451
266 | }
267 | }
268 | },
269 | "fontSize": {"magnitude": 14, "unit": "PT"}
270 | },
271 | "paragraphStyle": {
272 | "namedStyleType": "NORMAL_TEXT",
273 | "direction": "LEFT_TO_RIGHT",
274 | "spaceAbove": {"magnitude": 16, "unit": "PT"},
275 | "spaceBelow": {"magnitude": 4, "unit": "PT"},
276 | "keepLinesTogether": true,
277 | "keepWithNext": true
278 | }
279 | },
280 | {
281 | "namedStyleType": "HEADING_4",
282 | "textStyle": {
283 | "foregroundColor": {
284 | "color": {"rgbColor": {"red": 0.4, "green": 0.4, "blue": 0.4}}
285 | },
286 | "fontSize": {"magnitude": 12, "unit": "PT"}
287 | },
288 | "paragraphStyle": {
289 | "namedStyleType": "NORMAL_TEXT",
290 | "direction": "LEFT_TO_RIGHT",
291 | "spaceAbove": {"magnitude": 14, "unit": "PT"},
292 | "spaceBelow": {"magnitude": 4, "unit": "PT"},
293 | "keepLinesTogether": true,
294 | "keepWithNext": true
295 | }
296 | },
297 | {
298 | "namedStyleType": "HEADING_5",
299 | "textStyle": {
300 | "foregroundColor": {
301 | "color": {"rgbColor": {"red": 0.4, "green": 0.4, "blue": 0.4}}
302 | },
303 | "fontSize": {"magnitude": 11, "unit": "PT"}
304 | },
305 | "paragraphStyle": {
306 | "namedStyleType": "NORMAL_TEXT",
307 | "direction": "LEFT_TO_RIGHT",
308 | "spaceAbove": {"magnitude": 12, "unit": "PT"},
309 | "spaceBelow": {"magnitude": 4, "unit": "PT"},
310 | "keepLinesTogether": true,
311 | "keepWithNext": true
312 | }
313 | },
314 | {
315 | "namedStyleType": "HEADING_6",
316 | "textStyle": {
317 | "italic": true,
318 | "foregroundColor": {
319 | "color": {"rgbColor": {"red": 0.4, "green": 0.4, "blue": 0.4}}
320 | },
321 | "fontSize": {"magnitude": 11, "unit": "PT"}
322 | },
323 | "paragraphStyle": {
324 | "namedStyleType": "NORMAL_TEXT",
325 | "direction": "LEFT_TO_RIGHT",
326 | "spaceAbove": {"magnitude": 12, "unit": "PT"},
327 | "spaceBelow": {"magnitude": 4, "unit": "PT"},
328 | "keepLinesTogether": true,
329 | "keepWithNext": true
330 | }
331 | },
332 | {
333 | "namedStyleType": "TITLE",
334 | "textStyle": {"fontSize": {"magnitude": 26, "unit": "PT"}},
335 | "paragraphStyle": {
336 | "namedStyleType": "NORMAL_TEXT",
337 | "direction": "LEFT_TO_RIGHT",
338 | "spaceAbove": {"unit": "PT"},
339 | "spaceBelow": {"magnitude": 3, "unit": "PT"},
340 | "keepLinesTogether": true,
341 | "keepWithNext": true
342 | }
343 | },
344 | {
345 | "namedStyleType": "SUBTITLE",
346 | "textStyle": {
347 | "italic": false,
348 | "foregroundColor": {
349 | "color": {"rgbColor": {"red": 0.4, "green": 0.4, "blue": 0.4}}
350 | },
351 | "fontSize": {"magnitude": 15, "unit": "PT"},
352 | "weightedFontFamily": {"fontFamily": "Arial", "weight": 400}
353 | },
354 | "paragraphStyle": {
355 | "namedStyleType": "NORMAL_TEXT",
356 | "direction": "LEFT_TO_RIGHT",
357 | "spaceAbove": {"unit": "PT"},
358 | "spaceBelow": {"magnitude": 16, "unit": "PT"},
359 | "keepLinesTogether": true,
360 | "keepWithNext": true
361 | }
362 | }
363 | ]
364 | },
365 | "lists": {
366 | "kix.2ndctxh20b9f": {
367 | "listProperties": {
368 | "nestingLevels": [
369 | {
370 | "bulletAlignment": "START",
371 | "glyphSymbol": "●",
372 | "glyphFormat": "%0",
373 | "indentFirstLine": {"magnitude": 18, "unit": "PT"},
374 | "indentStart": {"magnitude": 36, "unit": "PT"},
375 | "textStyle": {"underline": false},
376 | "startNumber": 1
377 | },
378 | {
379 | "bulletAlignment": "START",
380 | "glyphSymbol": "○",
381 | "glyphFormat": "%1",
382 | "indentFirstLine": {"magnitude": 54, "unit": "PT"},
383 | "indentStart": {"magnitude": 72, "unit": "PT"},
384 | "textStyle": {"underline": false},
385 | "startNumber": 1
386 | },
387 | {
388 | "bulletAlignment": "START",
389 | "glyphSymbol": "■",
390 | "glyphFormat": "%2",
391 | "indentFirstLine": {"magnitude": 90, "unit": "PT"},
392 | "indentStart": {"magnitude": 108, "unit": "PT"},
393 | "textStyle": {"underline": false},
394 | "startNumber": 1
395 | },
396 | {
397 | "bulletAlignment": "START",
398 | "glyphSymbol": "●",
399 | "glyphFormat": "%3",
400 | "indentFirstLine": {"magnitude": 126, "unit": "PT"},
401 | "indentStart": {"magnitude": 144, "unit": "PT"},
402 | "textStyle": {"underline": false},
403 | "startNumber": 1
404 | },
405 | {
406 | "bulletAlignment": "START",
407 | "glyphSymbol": "○",
408 | "glyphFormat": "%4",
409 | "indentFirstLine": {"magnitude": 162, "unit": "PT"},
410 | "indentStart": {"magnitude": 180, "unit": "PT"},
411 | "textStyle": {"underline": false},
412 | "startNumber": 1
413 | },
414 | {
415 | "bulletAlignment": "START",
416 | "glyphSymbol": "■",
417 | "glyphFormat": "%5",
418 | "indentFirstLine": {"magnitude": 198, "unit": "PT"},
419 | "indentStart": {"magnitude": 216, "unit": "PT"},
420 | "textStyle": {"underline": false},
421 | "startNumber": 1
422 | },
423 | {
424 | "bulletAlignment": "START",
425 | "glyphSymbol": "●",
426 | "glyphFormat": "%6",
427 | "indentFirstLine": {"magnitude": 234, "unit": "PT"},
428 | "indentStart": {"magnitude": 252, "unit": "PT"},
429 | "textStyle": {"underline": false},
430 | "startNumber": 1
431 | },
432 | {
433 | "bulletAlignment": "START",
434 | "glyphSymbol": "○",
435 | "glyphFormat": "%7",
436 | "indentFirstLine": {"magnitude": 270, "unit": "PT"},
437 | "indentStart": {"magnitude": 288, "unit": "PT"},
438 | "textStyle": {"underline": false},
439 | "startNumber": 1
440 | },
441 | {
442 | "bulletAlignment": "START",
443 | "glyphSymbol": "■",
444 | "glyphFormat": "%8",
445 | "indentFirstLine": {"magnitude": 306, "unit": "PT"},
446 | "indentStart": {"magnitude": 324, "unit": "PT"},
447 | "textStyle": {"underline": false},
448 | "startNumber": 1
449 | }
450 | ]
451 | }
452 | }
453 | },
454 | "revisionId": "ALm37BXFZMyF6sAvtQkSYTvfnNMV8sGuiYDNrLQDSwG6crTJFRUN9OK3Ph4-6T-pUkz8nYz1m3tG8qoRK1icFA",
455 | "suggestionsViewMode": "SUGGESTIONS_INLINE",
456 | "documentId": "1BM_DE77JGmhxa4XrXXaDYuAjQGVjf3MCupETAgbUCQg"
457 | }
458 |
--------------------------------------------------------------------------------
/utils/google-document.js:
--------------------------------------------------------------------------------
1 | const json2md = require("json2md")
2 | const yamljs = require("yamljs")
3 | const _get = require("lodash/get")
4 | const _repeat = require("lodash/repeat")
5 | const _merge = require("lodash/merge")
6 |
7 | const {isCodeBlocks, isQuote} = require("./google-document-types")
8 | const {DEFAULT_OPTIONS} = require("./constants")
9 |
10 | const HORIZONTAL_TAB_CHAR = "\x09"
11 | const GOOGLE_DOCS_INDENT = 18
12 |
13 | class GoogleDocument {
14 | constructor({document, properties = {}, options = {}, links = {}}) {
15 | this.document = document
16 | this.links = links
17 | this.properties = properties
18 | this.options = _merge({}, DEFAULT_OPTIONS, options)
19 |
20 | this.cover = null
21 | this.elements = []
22 | this.headings = []
23 | this.footnotes = {}
24 | this.related = []
25 |
26 | // Keep the class scope in loops
27 | this.formatText = this.formatText.bind(this)
28 | this.normalizeElement = this.normalizeElement.bind(this)
29 |
30 | this.process()
31 | }
32 |
33 | formatText(el, {inlineImages = false, namedStyleType = "NORMAL_TEXT"} = {}) {
34 | if (el.inlineObjectElement) {
35 | const image = this.getImage(el)
36 | if (image) {
37 | if (inlineImages) {
38 | return ``
39 | }
40 | this.elements.push({
41 | type: "img",
42 | value: image,
43 | })
44 | }
45 | }
46 |
47 | // Person tag
48 | if (el.person) {
49 | return el.person.personProperties.name
50 | }
51 |
52 | // Rich link
53 | if (el.richLink) {
54 | const props = el.richLink.richLinkProperties
55 | return `[${props.title}](${props.uri})`
56 | }
57 |
58 | if (!el.textRun || !el.textRun.content || !el.textRun.content.trim()) {
59 | return ""
60 | }
61 |
62 | let before = ""
63 | let after = ""
64 | let text = el.textRun.content
65 | .replace(/\n$/, "") // Remove new lines
66 | .replace(/“|”/g, '"') // Replace smart quotes by double quotes
67 | .replace(/\u000b/g, "
") // Replace soft lines breaks, vertical tabs
68 | const contentMatch = text.match(/^(\s*)(\S+(?:[ \t\v]*\S+)*)(\s*)$/) // Match "text", "before" and "after"
69 |
70 | if (contentMatch) {
71 | before = contentMatch[1]
72 | text = contentMatch[2]
73 | after = contentMatch[3]
74 | }
75 |
76 | const defaultStyle = this.getTextStyle(namedStyleType)
77 | const textStyle = el.textRun.textStyle
78 | const style = this.options.keepDefaultStyle
79 | ? _merge({}, defaultStyle, textStyle)
80 | : textStyle
81 |
82 | const {
83 | backgroundColor,
84 | baselineOffset,
85 | bold,
86 | fontSize,
87 | foregroundColor,
88 | italic,
89 | link,
90 | strikethrough,
91 | underline,
92 | weightedFontFamily: {fontFamily} = {},
93 | } = style
94 |
95 | const isInlineCode = fontFamily === "Consolas"
96 | if (isInlineCode) {
97 | if (this.options.skipCodes) return text
98 |
99 | return "`" + text + "`"
100 | }
101 |
102 | const styles = []
103 |
104 | text = text.replace(/\*/g, "\\*") // Prevent * to be bold
105 | text = text.replace(/_/g, "\\_") // Prevent _ to be italic
106 |
107 | if (baselineOffset === "SUPERSCRIPT") {
108 | text = `${text}`
109 | }
110 |
111 | if (baselineOffset === "SUBSCRIPT") {
112 | text = `${text}`
113 | }
114 |
115 | if (underline && !link) {
116 | text = `${text}`
117 | }
118 |
119 | if (italic) {
120 | text = `_${text}_`
121 | }
122 |
123 | if (bold) {
124 | text = `**${text}**`
125 | }
126 |
127 | if (strikethrough) {
128 | text = `~~${text}~~`
129 | }
130 |
131 | if (fontSize) {
132 | const em = (fontSize.magnitude / this.bodyFontSize).toFixed(2)
133 | if (em !== "1.00") {
134 | styles.push(`font-size:${em}em`)
135 | }
136 | }
137 |
138 | if (_get(foregroundColor, ["color", "rgbColor"]) && !link) {
139 | const {rgbColor} = foregroundColor.color
140 | const red = Math.round((rgbColor.red || 0) * 255)
141 | const green = Math.round((rgbColor.green || 0) * 255)
142 | const blue = Math.round((rgbColor.blue || 0) * 255)
143 | if (red !== 0 || green !== 0 || blue !== 0) {
144 | styles.push(`color:rgb(${red}, ${green}, ${blue})`)
145 | }
146 | }
147 |
148 | if (_get(backgroundColor, ["color", "rgbColor"]) && !link) {
149 | const {rgbColor} = backgroundColor.color
150 | const red = Math.round((rgbColor.red || 0) * 255)
151 | const green = Math.round((rgbColor.green || 0) * 255)
152 | const blue = Math.round((rgbColor.blue || 0) * 255)
153 | styles.push(`background-color:rgb(${red}, ${green}, ${blue})`)
154 | }
155 |
156 | if (styles.length > 0) {
157 | text = `${text}`
158 | }
159 |
160 | if (link) {
161 | return `${before}[${text}](${link.url})${after}`
162 | }
163 |
164 | return before + text + after
165 | }
166 |
167 | getTextStyle(type) {
168 | const documentStyles = _get(this.document, ["namedStyles", "styles"])
169 |
170 | if (!documentStyles) return {}
171 |
172 | const style = documentStyles.find((style) => style.namedStyleType === type)
173 | return style.textStyle
174 | }
175 |
176 | getImage(el) {
177 | if (this.options.skipImages) return
178 |
179 | const {inlineObjects} = this.document
180 |
181 | if (!inlineObjects || !el.inlineObjectElement) {
182 | return
183 | }
184 |
185 | const inlineObject = inlineObjects[el.inlineObjectElement.inlineObjectId]
186 | const embeddedObject = inlineObject.inlineObjectProperties.embeddedObject
187 |
188 | return {
189 | source: embeddedObject.imageProperties.contentUri,
190 | title: embeddedObject.title || "",
191 | alt: embeddedObject.description || "",
192 | }
193 | }
194 |
195 | processCover() {
196 | const {headers, documentStyle} = this.document
197 | const firstPageHeaderId = _get(documentStyle, ["firstPageHeaderId"])
198 |
199 | if (!firstPageHeaderId) {
200 | return
201 | }
202 |
203 | const headerElement = _get(headers[firstPageHeaderId], [
204 | "content",
205 | 0,
206 | "paragraph",
207 | "elements",
208 | 0,
209 | ])
210 |
211 | const image = this.getImage(headerElement)
212 |
213 | if (image) {
214 | this.cover = {
215 | image: image.source,
216 | title: image.title,
217 | alt: image.alt,
218 | }
219 | }
220 | }
221 |
222 | getTableCellContent(content) {
223 | return content
224 | .map((contentElement) => {
225 | const hasParagraph = contentElement.paragraph
226 |
227 | if (!hasParagraph) return ""
228 | return contentElement.paragraph.elements.map(this.formatText).join("")
229 | })
230 | .join("")
231 | }
232 |
233 | indentText(text, level) {
234 | return `${_repeat(HORIZONTAL_TAB_CHAR, level)}${text}`
235 | }
236 |
237 | stringifyContent(tagContent) {
238 | return tagContent.join("").replace(/\n$/, "")
239 | }
240 |
241 | appendToList({list, listItem, elementLevel, level}) {
242 | const lastItem = list[list.length - 1]
243 |
244 | if (listItem.level > level) {
245 | if (typeof lastItem === "object") {
246 | this.appendToList({
247 | list: lastItem.value,
248 | listItem,
249 | elementLevel,
250 | level: level + 1,
251 | })
252 | } else {
253 | list.push({
254 | type: listItem.tag,
255 | value: [listItem.text],
256 | })
257 | }
258 | } else {
259 | list.push(listItem.text)
260 | }
261 | }
262 |
263 | getListTag(listId, level) {
264 | const glyph = _get(this.document, [
265 | "lists",
266 | listId,
267 | "listProperties",
268 | "nestingLevels",
269 | level,
270 | "glyphType",
271 | ])
272 |
273 | return glyph ? "ol" : "ul"
274 | }
275 |
276 | processList(paragraph, index) {
277 | if (this.options.skipLists) return
278 |
279 | const prevListId = _get(this.document, [
280 | "body",
281 | "content",
282 | index - 1,
283 | "paragraph",
284 | "bullet",
285 | "listId",
286 | ])
287 | const isPrevList = prevListId === paragraph.bullet.listId
288 | const prevList = _get(this.elements, [this.elements.length - 1, "value"])
289 | const text = this.stringifyContent(
290 | paragraph.elements.map((el) => this.formatText(el, {inlineImages: true}))
291 | )
292 |
293 | if (isPrevList && Array.isArray(prevList)) {
294 | const {nestingLevel} = paragraph.bullet
295 |
296 | if (nestingLevel) {
297 | this.appendToList({
298 | list: prevList,
299 | listItem: {
300 | text,
301 | level: nestingLevel,
302 | tag: this.getListTag(paragraph.bullet.listId, prevList.length),
303 | },
304 | level: 0,
305 | })
306 | } else {
307 | prevList.push(text)
308 | }
309 | } else {
310 | this.elements.push({
311 | type: this.getListTag(paragraph.bullet.listId, 0),
312 | value: [text],
313 | })
314 | }
315 | }
316 |
317 | processParagraph(paragraph, index) {
318 | const tags = {
319 | HEADING_1: "h1",
320 | HEADING_2: "h2",
321 | HEADING_3: "h3",
322 | HEADING_4: "h4",
323 | HEADING_5: "h5",
324 | HEADING_6: "h6",
325 | NORMAL_TEXT: "p",
326 | SUBTITLE: "h2",
327 | TITLE: "h1",
328 | }
329 | const namedStyleType = paragraph.paragraphStyle.namedStyleType
330 | const tag = tags[namedStyleType]
331 | const isHeading = tag.startsWith("h")
332 |
333 | // Lists
334 | if (paragraph.bullet) {
335 | this.processList(paragraph, index)
336 | return
337 | }
338 |
339 | let tagContentArray = []
340 |
341 | paragraph.elements.forEach((el) => {
342 | if (el.pageBreak) {
343 | return
344 | }
345 |
346 | //
347 | else if (el.horizontalRule) {
348 | tagContentArray.push("
")
349 | }
350 |
351 | // Footnotes
352 | else if (el.footnoteReference) {
353 | if (this.options.skipFootnotes) return
354 |
355 | tagContentArray.push(`[^${el.footnoteReference.footnoteNumber}]`)
356 | this.footnotes[el.footnoteReference.footnoteId] =
357 | el.footnoteReference.footnoteNumber
358 | }
359 |
360 | // Headings
361 | else if (isHeading) {
362 | if (this.options.skipHeadings) return
363 |
364 | const text = this.formatText(el, {
365 | namedStyleType,
366 | })
367 |
368 | if (text) {
369 | tagContentArray.push(text)
370 | }
371 | }
372 |
373 | // Texts
374 | else {
375 | const text = this.formatText(el)
376 |
377 | if (text) {
378 | tagContentArray.push(text)
379 | }
380 | }
381 | })
382 |
383 | if (tagContentArray.length === 0) return
384 |
385 | let content = this.stringifyContent(tagContentArray)
386 | let tagIndentLevel = 0
387 |
388 | if (paragraph.paragraphStyle.indentStart) {
389 | const {magnitude} = paragraph.paragraphStyle.indentStart
390 | tagIndentLevel = Math.round(magnitude / GOOGLE_DOCS_INDENT)
391 | }
392 |
393 | if (tagIndentLevel > 0) {
394 | content = this.indentText(content, tagIndentLevel)
395 | }
396 |
397 | this.elements.push({
398 | type: tag,
399 | value: content,
400 | })
401 |
402 | if (isHeading) {
403 | this.headings.push({tag, text: content, index: this.elements.length - 1})
404 | }
405 | }
406 |
407 | processQuote(table) {
408 | if (this.options.skipQuotes) return
409 |
410 | const firstRow = table.tableRows[0]
411 | const firstCell = firstRow.tableCells[0]
412 | const quote = this.getTableCellContent(firstCell.content)
413 | const blockquote = quote.replace(/“|”/g, "") // Delete smart-quotes
414 |
415 | this.elements.push({type: "blockquote", value: blockquote})
416 | }
417 |
418 | processCode(table) {
419 | if (this.options.skipCodes) return
420 |
421 | const firstRow = table.tableRows[0]
422 | const firstCell = firstRow.tableCells[0]
423 | const codeContent = firstCell.content
424 | .map(({paragraph}) =>
425 | paragraph.elements.map((el) => el.textRun.content).join("")
426 | )
427 | .join("") // Transform to string
428 | .replace(/\x0B/g, "\n") // Replace vertical tabs
429 | .replace(/^\n|\n$/g, "") // Remove new lines characters at the beginning and end of line
430 | .split("\n") // Transform to array
431 |
432 | // "".split() -> [""]
433 | if (codeContent.length === 1 && codeContent[0] === "") return
434 |
435 | let lang = null
436 | const langMatch = codeContent[0].match(/^\s*lang:\s*(.*)$/)
437 |
438 | if (langMatch) {
439 | codeContent.shift()
440 | lang = langMatch[1]
441 | }
442 |
443 | this.elements.push({
444 | type: "code",
445 | value: {
446 | language: lang,
447 | content: codeContent,
448 | },
449 | })
450 | }
451 |
452 | processTable(table) {
453 | if (this.options.skipTables) return
454 |
455 | const [thead, ...tbody] = table.tableRows
456 |
457 | this.elements.push({
458 | type: "table",
459 | value: {
460 | headers: thead.tableCells.map(({content}) =>
461 | this.getTableCellContent(content)
462 | ),
463 | rows: tbody.map((row) =>
464 | row.tableCells.map(({content}) => this.getTableCellContent(content))
465 | ),
466 | },
467 | })
468 | }
469 |
470 | processFootnotes() {
471 | if (this.options.skipFootnotes) return
472 |
473 | const footnotes = []
474 | const documentFootnotes = this.document.footnotes
475 |
476 | if (!documentFootnotes) return
477 |
478 | Object.entries(documentFootnotes).forEach(([, value]) => {
479 | const paragraphElements = value.content[0].paragraph.elements
480 | const tagContentArray = paragraphElements.map(this.formatText)
481 | const tagContentString = this.stringifyContent(tagContentArray)
482 |
483 | footnotes.push({
484 | type: "footnote",
485 | value: {
486 | number: this.footnotes[value.footnoteId],
487 | text: tagContentString,
488 | },
489 | })
490 | })
491 |
492 | footnotes.sort(
493 | (footnote1, footnote2) =>
494 | parseInt(footnote1.value.number) - parseInt(footnote2.value.number)
495 | )
496 |
497 | this.elements.push(...footnotes)
498 | }
499 |
500 | processDemoteHeadings() {
501 | this.headings.forEach((heading) => {
502 | const levelevel = Number(heading.tag.substring(1))
503 | const newLevel = levelevel < 6 ? levelevel + 1 : levelevel
504 | this.elements[heading.index] = {type: "h" + newLevel, value: heading.text}
505 | })
506 | }
507 |
508 | processInternalLinks() {
509 | if (Object.keys(this.links).length > 0) {
510 | const elementsStringified = JSON.stringify(this.elements)
511 |
512 | const elementsStringifiedWithRelativePaths = elementsStringified.replace(
513 | /https:\/\/docs.google.com\/document\/(?:u\/\d+\/)?d\/([a-zA-Z0-9_-]+)(?:\/edit|\/preview)?/g,
514 | (match, id) => {
515 | if (this.links[id]) {
516 | this.related.push(id)
517 | return this.links[id]
518 | }
519 |
520 | return match
521 | }
522 | )
523 |
524 | this.elements = JSON.parse(elementsStringifiedWithRelativePaths)
525 | }
526 | }
527 |
528 | process() {
529 | this.bodyFontSize = _get(
530 | this.getTextStyle("NORMAL_TEXT"),
531 | "fontSize.magnitude"
532 | )
533 |
534 | this.processCover()
535 |
536 | this.document.body.content.forEach(
537 | ({paragraph, table, sectionBreak, tableOfContents}, i) => {
538 | // Unsupported elements
539 | if (sectionBreak || tableOfContents) {
540 | return
541 | }
542 |
543 | if (table) {
544 | // Quotes
545 | if (isQuote(table)) {
546 | this.processQuote(table)
547 | }
548 |
549 | // Code Blocks
550 | else if (isCodeBlocks(table)) {
551 | this.processCode(table)
552 | }
553 |
554 | // Tables
555 | else {
556 | this.processTable(table)
557 | }
558 | }
559 |
560 | // Paragraphs
561 | else {
562 | this.processParagraph(paragraph, i)
563 | }
564 | }
565 | )
566 |
567 | // Footnotes
568 | this.processFootnotes()
569 |
570 | // h1 -> h2, h2 -> h3, ...
571 | if (this.options.demoteHeadings === true) {
572 | this.processDemoteHeadings()
573 | }
574 |
575 | this.processInternalLinks()
576 | }
577 |
578 | normalizeElement(element) {
579 | if (element.type && element.value) {
580 | return {[element.type]: this.normalizeElement(element.value)}
581 | }
582 |
583 | if (Array.isArray(element)) {
584 | return element.map(this.normalizeElement)
585 | }
586 |
587 | return element
588 | }
589 |
590 | toMarkdown() {
591 | const frontmatter = {
592 | ...this.properties,
593 | ...(this.cover ? {cover: this.cover} : {}),
594 | }
595 | const json = this.elements.map(this.normalizeElement)
596 | const markdownContent = json2md(json)
597 | const markdownFrontmatter =
598 | Object.keys(frontmatter).length > 0
599 | ? `---\n${yamljs.stringify(frontmatter)}---\n`
600 | : ""
601 |
602 | return `${markdownFrontmatter}${markdownContent}`
603 | }
604 | }
605 |
606 | // Add extra converter for footnotes
607 | json2md.converters.footnote = function (footnote) {
608 | return `[^${footnote.number}]:${footnote.text}`
609 | }
610 |
611 | module.exports = {
612 | GoogleDocument: GoogleDocument,
613 | }
614 |
--------------------------------------------------------------------------------
/__tests__/documents/links.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Links",
3 | "body": {
4 | "content": [
5 | {
6 | "endIndex": 1,
7 | "sectionBreak": {
8 | "sectionStyle": {
9 | "columnSeparatorStyle": "NONE",
10 | "contentDirection": "LEFT_TO_RIGHT",
11 | "sectionType": "CONTINUOUS"
12 | }
13 | }
14 | },
15 | {
16 | "startIndex": 1,
17 | "endIndex": 9,
18 | "paragraph": {
19 | "elements": [
20 | {
21 | "startIndex": 1,
22 | "endIndex": 8,
23 | "textRun": {
24 | "content": "to self",
25 | "textStyle": {
26 | "underline": true,
27 | "foregroundColor": {
28 | "color": {
29 | "rgbColor": {
30 | "red": 0.06666667,
31 | "green": 0.33333334,
32 | "blue": 0.8
33 | }
34 | }
35 | },
36 | "link": {
37 | "url": "https://docs.google.com/document/d/1UuBxtIEEVh98wyBR9fMmLqzJEkmNlAMMoC4SEhprHfQ"
38 | }
39 | }
40 | }
41 | },
42 | {
43 | "startIndex": 8,
44 | "endIndex": 9,
45 | "textRun": {
46 | "content": "\n",
47 | "textStyle": {}
48 | }
49 | }
50 | ],
51 | "paragraphStyle": {
52 | "namedStyleType": "NORMAL_TEXT",
53 | "direction": "LEFT_TO_RIGHT"
54 | }
55 | }
56 | },
57 | {
58 | "startIndex": 9,
59 | "endIndex": 10,
60 | "paragraph": {
61 | "elements": [
62 | {
63 | "startIndex": 9,
64 | "endIndex": 10,
65 | "textRun": {
66 | "content": "\n",
67 | "textStyle": {}
68 | }
69 | }
70 | ],
71 | "paragraphStyle": {
72 | "namedStyleType": "NORMAL_TEXT",
73 | "direction": "LEFT_TO_RIGHT"
74 | }
75 | }
76 | },
77 | {
78 | "startIndex": 10,
79 | "endIndex": 31,
80 | "paragraph": {
81 | "elements": [
82 | {
83 | "startIndex": 10,
84 | "endIndex": 30,
85 | "textRun": {
86 | "content": "to self with user id",
87 | "textStyle": {
88 | "underline": true,
89 | "foregroundColor": {
90 | "color": {
91 | "rgbColor": {
92 | "red": 0.06666667,
93 | "green": 0.33333334,
94 | "blue": 0.8
95 | }
96 | }
97 | },
98 | "link": {
99 | "url": "https://docs.google.com/document/u/1/d/1UuBxtIEEVh98wyBR9fMmLqzJEkmNlAMMoC4SEhprHfQ"
100 | }
101 | }
102 | }
103 | },
104 | {
105 | "startIndex": 30,
106 | "endIndex": 31,
107 | "textRun": {
108 | "content": "\n",
109 | "textStyle": {}
110 | }
111 | }
112 | ],
113 | "paragraphStyle": {
114 | "namedStyleType": "NORMAL_TEXT",
115 | "direction": "LEFT_TO_RIGHT"
116 | }
117 | }
118 | },
119 | {
120 | "startIndex": 31,
121 | "endIndex": 32,
122 | "paragraph": {
123 | "elements": [
124 | {
125 | "startIndex": 31,
126 | "endIndex": 32,
127 | "textRun": {
128 | "content": "\n",
129 | "textStyle": {}
130 | }
131 | }
132 | ],
133 | "paragraphStyle": {
134 | "namedStyleType": "NORMAL_TEXT",
135 | "direction": "LEFT_TO_RIGHT"
136 | }
137 | }
138 | },
139 | {
140 | "startIndex": 32,
141 | "endIndex": 50,
142 | "paragraph": {
143 | "elements": [
144 | {
145 | "startIndex": 32,
146 | "endIndex": 49,
147 | "textRun": {
148 | "content": "to self with edit",
149 | "textStyle": {
150 | "underline": true,
151 | "foregroundColor": {
152 | "color": {
153 | "rgbColor": {
154 | "red": 0.06666667,
155 | "green": 0.33333334,
156 | "blue": 0.8
157 | }
158 | }
159 | },
160 | "link": {
161 | "url": "https://docs.google.com/document/d/1UuBxtIEEVh98wyBR9fMmLqzJEkmNlAMMoC4SEhprHfQ/edit"
162 | }
163 | }
164 | }
165 | },
166 | {
167 | "startIndex": 49,
168 | "endIndex": 50,
169 | "textRun": {
170 | "content": "\n",
171 | "textStyle": {}
172 | }
173 | }
174 | ],
175 | "paragraphStyle": {
176 | "namedStyleType": "NORMAL_TEXT",
177 | "direction": "LEFT_TO_RIGHT"
178 | }
179 | }
180 | },
181 | {
182 | "startIndex": 50,
183 | "endIndex": 51,
184 | "paragraph": {
185 | "elements": [
186 | {
187 | "startIndex": 50,
188 | "endIndex": 51,
189 | "textRun": {
190 | "content": "\n",
191 | "textStyle": {}
192 | }
193 | }
194 | ],
195 | "paragraphStyle": {
196 | "namedStyleType": "NORMAL_TEXT",
197 | "direction": "LEFT_TO_RIGHT"
198 | }
199 | }
200 | },
201 | {
202 | "startIndex": 51,
203 | "endIndex": 72,
204 | "paragraph": {
205 | "elements": [
206 | {
207 | "startIndex": 51,
208 | "endIndex": 71,
209 | "textRun": {
210 | "content": "to self with preview",
211 | "textStyle": {
212 | "underline": true,
213 | "foregroundColor": {
214 | "color": {
215 | "rgbColor": {
216 | "red": 0.06666667,
217 | "green": 0.33333334,
218 | "blue": 0.8
219 | }
220 | }
221 | },
222 | "link": {
223 | "url": "https://docs.google.com/document/d/1UuBxtIEEVh98wyBR9fMmLqzJEkmNlAMMoC4SEhprHfQ/preview"
224 | }
225 | }
226 | }
227 | },
228 | {
229 | "startIndex": 71,
230 | "endIndex": 72,
231 | "textRun": {
232 | "content": "\n",
233 | "textStyle": {}
234 | }
235 | }
236 | ],
237 | "paragraphStyle": {
238 | "namedStyleType": "NORMAL_TEXT",
239 | "direction": "LEFT_TO_RIGHT"
240 | }
241 | }
242 | },
243 | {
244 | "startIndex": 73,
245 | "endIndex": 93,
246 | "paragraph": {
247 | "elements": [
248 | {
249 | "startIndex": 51,
250 | "endIndex": 71,
251 | "textRun": {
252 | "content": "unknown url",
253 | "textStyle": {
254 | "underline": true,
255 | "foregroundColor": {
256 | "color": {
257 | "rgbColor": {
258 | "red": 0.06666667,
259 | "green": 0.33333334,
260 | "blue": 0.8
261 | }
262 | }
263 | },
264 | "link": {
265 | "url": "https://docs.google.com/document/d/unknown"
266 | }
267 | }
268 | }
269 | },
270 | {
271 | "startIndex": 71,
272 | "endIndex": 72,
273 | "textRun": {
274 | "content": "\n",
275 | "textStyle": {}
276 | }
277 | }
278 | ],
279 | "paragraphStyle": {
280 | "namedStyleType": "NORMAL_TEXT",
281 | "direction": "LEFT_TO_RIGHT"
282 | }
283 | }
284 | }
285 | ]
286 | },
287 | "documentStyle": {
288 | "background": {
289 | "color": {}
290 | },
291 | "pageNumberStart": 1,
292 | "marginTop": {
293 | "magnitude": 72,
294 | "unit": "PT"
295 | },
296 | "marginBottom": {
297 | "magnitude": 72,
298 | "unit": "PT"
299 | },
300 | "marginRight": {
301 | "magnitude": 72,
302 | "unit": "PT"
303 | },
304 | "marginLeft": {
305 | "magnitude": 72,
306 | "unit": "PT"
307 | },
308 | "pageSize": {
309 | "height": {
310 | "magnitude": 792,
311 | "unit": "PT"
312 | },
313 | "width": {
314 | "magnitude": 612,
315 | "unit": "PT"
316 | }
317 | },
318 | "marginHeader": {
319 | "magnitude": 36,
320 | "unit": "PT"
321 | },
322 | "marginFooter": {
323 | "magnitude": 36,
324 | "unit": "PT"
325 | },
326 | "useCustomHeaderFooterMargins": true
327 | },
328 | "namedStyles": {
329 | "styles": [
330 | {
331 | "namedStyleType": "NORMAL_TEXT",
332 | "textStyle": {
333 | "bold": false,
334 | "italic": false,
335 | "underline": false,
336 | "strikethrough": false,
337 | "smallCaps": false,
338 | "backgroundColor": {},
339 | "foregroundColor": {
340 | "color": {
341 | "rgbColor": {}
342 | }
343 | },
344 | "fontSize": {
345 | "magnitude": 11,
346 | "unit": "PT"
347 | },
348 | "weightedFontFamily": {
349 | "fontFamily": "Arial",
350 | "weight": 400
351 | },
352 | "baselineOffset": "NONE"
353 | },
354 | "paragraphStyle": {
355 | "namedStyleType": "NORMAL_TEXT",
356 | "alignment": "START",
357 | "lineSpacing": 115,
358 | "direction": "LEFT_TO_RIGHT",
359 | "spacingMode": "COLLAPSE_LISTS",
360 | "spaceAbove": {
361 | "unit": "PT"
362 | },
363 | "spaceBelow": {
364 | "unit": "PT"
365 | },
366 | "borderBetween": {
367 | "color": {},
368 | "width": {
369 | "unit": "PT"
370 | },
371 | "padding": {
372 | "unit": "PT"
373 | },
374 | "dashStyle": "SOLID"
375 | },
376 | "borderTop": {
377 | "color": {},
378 | "width": {
379 | "unit": "PT"
380 | },
381 | "padding": {
382 | "unit": "PT"
383 | },
384 | "dashStyle": "SOLID"
385 | },
386 | "borderBottom": {
387 | "color": {},
388 | "width": {
389 | "unit": "PT"
390 | },
391 | "padding": {
392 | "unit": "PT"
393 | },
394 | "dashStyle": "SOLID"
395 | },
396 | "borderLeft": {
397 | "color": {},
398 | "width": {
399 | "unit": "PT"
400 | },
401 | "padding": {
402 | "unit": "PT"
403 | },
404 | "dashStyle": "SOLID"
405 | },
406 | "borderRight": {
407 | "color": {},
408 | "width": {
409 | "unit": "PT"
410 | },
411 | "padding": {
412 | "unit": "PT"
413 | },
414 | "dashStyle": "SOLID"
415 | },
416 | "indentFirstLine": {
417 | "unit": "PT"
418 | },
419 | "indentStart": {
420 | "unit": "PT"
421 | },
422 | "indentEnd": {
423 | "unit": "PT"
424 | },
425 | "keepLinesTogether": false,
426 | "keepWithNext": false,
427 | "avoidWidowAndOrphan": true,
428 | "shading": {
429 | "backgroundColor": {}
430 | }
431 | }
432 | },
433 | {
434 | "namedStyleType": "HEADING_1",
435 | "textStyle": {
436 | "fontSize": {
437 | "magnitude": 20,
438 | "unit": "PT"
439 | }
440 | },
441 | "paragraphStyle": {
442 | "namedStyleType": "NORMAL_TEXT",
443 | "direction": "LEFT_TO_RIGHT",
444 | "spaceAbove": {
445 | "magnitude": 20,
446 | "unit": "PT"
447 | },
448 | "spaceBelow": {
449 | "magnitude": 6,
450 | "unit": "PT"
451 | },
452 | "keepLinesTogether": true,
453 | "keepWithNext": true
454 | }
455 | },
456 | {
457 | "namedStyleType": "HEADING_2",
458 | "textStyle": {
459 | "bold": true,
460 | "underline": true
461 | },
462 | "paragraphStyle": {
463 | "headingId": "h.crrpgju8ws84",
464 | "namedStyleType": "HEADING_2",
465 | "direction": "LEFT_TO_RIGHT",
466 | "spaceAbove": {
467 | "magnitude": 18,
468 | "unit": "PT"
469 | },
470 | "spaceBelow": {
471 | "magnitude": 6,
472 | "unit": "PT"
473 | },
474 | "keepLinesTogether": true,
475 | "keepWithNext": true
476 | }
477 | },
478 | {
479 | "namedStyleType": "HEADING_3",
480 | "textStyle": {
481 | "bold": true
482 | },
483 | "paragraphStyle": {
484 | "headingId": "h.yrkcs963nbig",
485 | "namedStyleType": "HEADING_3",
486 | "direction": "LEFT_TO_RIGHT",
487 | "spaceAbove": {
488 | "magnitude": 16,
489 | "unit": "PT"
490 | },
491 | "spaceBelow": {
492 | "magnitude": 4,
493 | "unit": "PT"
494 | },
495 | "keepLinesTogether": true,
496 | "keepWithNext": true
497 | }
498 | },
499 | {
500 | "namedStyleType": "HEADING_4",
501 | "textStyle": {
502 | "underline": true
503 | },
504 | "paragraphStyle": {
505 | "headingId": "h.2pgom3u1ze8f",
506 | "namedStyleType": "HEADING_4",
507 | "direction": "LEFT_TO_RIGHT",
508 | "spaceAbove": {
509 | "magnitude": 14,
510 | "unit": "PT"
511 | },
512 | "spaceBelow": {
513 | "magnitude": 4,
514 | "unit": "PT"
515 | },
516 | "keepLinesTogether": true,
517 | "keepWithNext": true
518 | }
519 | },
520 | {
521 | "namedStyleType": "HEADING_5",
522 | "textStyle": {
523 | "foregroundColor": {
524 | "color": {
525 | "rgbColor": {
526 | "red": 0.4,
527 | "green": 0.4,
528 | "blue": 0.4
529 | }
530 | }
531 | },
532 | "fontSize": {
533 | "magnitude": 11,
534 | "unit": "PT"
535 | }
536 | },
537 | "paragraphStyle": {
538 | "namedStyleType": "NORMAL_TEXT",
539 | "direction": "LEFT_TO_RIGHT",
540 | "spaceAbove": {
541 | "magnitude": 12,
542 | "unit": "PT"
543 | },
544 | "spaceBelow": {
545 | "magnitude": 4,
546 | "unit": "PT"
547 | },
548 | "keepLinesTogether": true,
549 | "keepWithNext": true
550 | }
551 | },
552 | {
553 | "namedStyleType": "HEADING_6",
554 | "textStyle": {
555 | "italic": true,
556 | "foregroundColor": {
557 | "color": {
558 | "rgbColor": {
559 | "red": 0.4,
560 | "green": 0.4,
561 | "blue": 0.4
562 | }
563 | }
564 | },
565 | "fontSize": {
566 | "magnitude": 11,
567 | "unit": "PT"
568 | }
569 | },
570 | "paragraphStyle": {
571 | "namedStyleType": "NORMAL_TEXT",
572 | "direction": "LEFT_TO_RIGHT",
573 | "spaceAbove": {
574 | "magnitude": 12,
575 | "unit": "PT"
576 | },
577 | "spaceBelow": {
578 | "magnitude": 4,
579 | "unit": "PT"
580 | },
581 | "keepLinesTogether": true,
582 | "keepWithNext": true
583 | }
584 | },
585 | {
586 | "namedStyleType": "TITLE",
587 | "textStyle": {
588 | "bold": true,
589 | "fontSize": {
590 | "magnitude": 18,
591 | "unit": "PT"
592 | }
593 | },
594 | "paragraphStyle": {
595 | "headingId": "h.v0sy8ume7683",
596 | "namedStyleType": "TITLE",
597 | "alignment": "CENTER",
598 | "direction": "LEFT_TO_RIGHT",
599 | "spaceBelow": {
600 | "magnitude": 3,
601 | "unit": "PT"
602 | },
603 | "keepLinesTogether": true,
604 | "keepWithNext": true
605 | }
606 | },
607 | {
608 | "namedStyleType": "SUBTITLE",
609 | "textStyle": {
610 | "italic": false,
611 | "foregroundColor": {
612 | "color": {
613 | "rgbColor": {
614 | "red": 0.4,
615 | "green": 0.4,
616 | "blue": 0.4
617 | }
618 | }
619 | },
620 | "fontSize": {
621 | "magnitude": 15,
622 | "unit": "PT"
623 | },
624 | "weightedFontFamily": {
625 | "fontFamily": "Arial",
626 | "weight": 400
627 | }
628 | },
629 | "paragraphStyle": {
630 | "namedStyleType": "NORMAL_TEXT",
631 | "direction": "LEFT_TO_RIGHT",
632 | "spaceAbove": {
633 | "unit": "PT"
634 | },
635 | "spaceBelow": {
636 | "magnitude": 16,
637 | "unit": "PT"
638 | },
639 | "keepLinesTogether": true,
640 | "keepWithNext": true
641 | }
642 | }
643 | ]
644 | },
645 | "revisionId": "ALm37BX_6dRY7ymQxjaUV7wPyC3GsVLNFxPNp7tlKNo6tshN9jZCFXNFhN0kAnzlUJi8684QZ9uDa4I72wQq7w",
646 | "suggestionsViewMode": "SUGGESTIONS_INLINE",
647 | "documentId": "1UuBxtIEEVh98wyBR9fMmLqzJEkmNlAMMoC4SEhprHfQ"
648 | }
649 |
--------------------------------------------------------------------------------