├── utils.gs ├── images ├── fig.png ├── figur.png ├── Panel_small.png └── crossref_icon_128.png ├── lof-loader.html ├── changelog.md ├── privacy.md ├── LICENSE ├── terms-of-service.md ├── loader-style.html ├── error.gs ├── lof.html ├── sidebar.gs ├── test.gs ├── main.gs ├── manual_tests.md ├── lof-config.html ├── settings.test.gs ├── README.md ├── text.test.gs ├── sidebar-style.html ├── text.gs ├── settings.gs ├── sidebar-html.html ├── lof-code.gs └── sidebar-js.html /utils.gs: -------------------------------------------------------------------------------- 1 | // repair copied links 2 | -------------------------------------------------------------------------------- /images/fig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidrthorn/cross_reference/HEAD/images/fig.png -------------------------------------------------------------------------------- /images/figur.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidrthorn/cross_reference/HEAD/images/figur.png -------------------------------------------------------------------------------- /images/Panel_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidrthorn/cross_reference/HEAD/images/Panel_small.png -------------------------------------------------------------------------------- /images/crossref_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidrthorn/cross_reference/HEAD/images/crossref_icon_128.png -------------------------------------------------------------------------------- /lof-loader.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v2 4 | 5 | - capitalisation is left entirely to the user. When replacement text is lowercase, capitalisation is 6 | based on the text being replaced. 7 | - list of figures show the entire paragraph that contains the label 8 | - support for suffixes -------------------------------------------------------------------------------- /privacy.md: -------------------------------------------------------------------------------- 1 | # Cross Reference for Google Docs 2 | 3 | ## Privacy Policy 4 | Cross Reference makes changes to Google Docs documents and stores user settings in the users Google Docs property stores (document stores and user stores). Cross Reference neither exports nor collects any user data. 5 | 6 | The list of figures feature in Cross Reference uses the PDF.js library to process documents as PDFs. However, this library is run within the add-on instance in the document and does not export the PDF to any external servers. 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 David Rowthorn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /terms-of-service.md: -------------------------------------------------------------------------------- 1 | # Terms of Service for Cross Reference 2 | 3 | By installing and using the Cross Reference Google Docs add-on, you agree to these terms. 4 | 5 | ### Disclaimer of Warranties 6 | This plugin is provided "as is" and "as available," without any warranties of any kind, 7 | either express or implied, including but not limited to warranties of merchantability, 8 | fitness for a particular purpose, or non-infringement. We do not warrant that the plugin 9 | will be uninterrupted, error-free, or free of harmful components. 10 | 11 | ### Limitation of Liability 12 | In no event shall we be liable for any direct, indirect, incidental, special, consequential, 13 | or exemplary damages, including but not limited to, damages for loss of profits, goodwill, data, 14 | or other intangible losses, resulting from the use or inability to use the plugin, even if 15 | we have been advised of the possibility of such damages. 16 | 17 | ### Your Responsibility 18 | You acknowledge and agree that your use of the plugin is solely at your own risk. 19 | You are responsible for ensuring that your documents are backed up and for 20 | taking appropriate precautions to prevent any loss of data or disruption to your work. 21 | -------------------------------------------------------------------------------- /loader-style.html: -------------------------------------------------------------------------------- 1 | 76 | -------------------------------------------------------------------------------- /error.gs: -------------------------------------------------------------------------------- 1 | function handleErr(err) { 2 | addFlag(err.text, err.CRUrl) 3 | DocumentApp.getUi().alert(err.message) 4 | } 5 | 6 | 7 | function addFlag(text, CRUrl) { 8 | if (Number.isNaN(CRUrl.start) || Number.isNaN(CRUrl.end)) return 9 | 10 | const doc = DocumentApp.getActiveDocument() 11 | const position = doc.newPosition(text, CRUrl.start) 12 | 13 | text.setForegroundColor(CRUrl.start, CRUrl.end, '#FF0000') 14 | doc.setCursor(position) 15 | } 16 | 17 | 18 | function CRError(containingText, CRUrl, error) { 19 | const url = CRUrl.url 20 | const errorBoilerplate = "\n\nClick 'OK' to see the problem highlighted. Updating the document when this " + 21 | '\nhas been fixed will automatically remove this highlighting.' 22 | 23 | const messages = { 24 | duplicate: 'There are at least 2 labels with the code ' + url + '.' + 25 | "\n\nLabel codes must be 5 letters and label names (e.g. '" + 26 | url.substr(7) + "') must be unique." + 27 | errorBoilerplate, 28 | missref: 'There is a reference with nothing to refer to.' + 29 | '\nIt might contain a typo or the corresponding label might be missing.' + 30 | errorBoilerplate, 31 | unrecognised: 'A label or reference code was not recognised.' + 32 | '\n\nIt might be a typo or it might be a custom label you' + 33 | '\nhave not yet added in the configuration sidebar.' + 34 | errorBoilerplate, 35 | } 36 | 37 | return { 38 | text: containingText, 39 | CRUrl: CRUrl, 40 | error: error, 41 | message: messages[error], 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lof.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | Long, complex documents can
take minutes to process
15 | 16 | -------------------------------------------------------------------------------- /sidebar.gs: -------------------------------------------------------------------------------- 1 | function updateProps(tempSettings) { 2 | const docProps = PropertiesService.getDocumentProperties() 3 | for (const labCode in tempSettings) { 4 | docProps.setProperty(getPropKey(labCode), encodeSetting(tempSettings[labCode])) 5 | } 6 | updateDoc() 7 | return '#save' 8 | } 9 | 10 | 11 | function storeDefault(tempSettings) { 12 | const userProps = PropertiesService.getUserProperties() 13 | for (const labCode in tempSettings) { 14 | const setting = tempSettings[labCode] 15 | userProps.setProperty(getPropKey(labCode), encodeSetting(setting)) 16 | } 17 | return '#save-defaults' 18 | } 19 | 20 | 21 | const storeCustom = setting => PropertiesService.getUserProperties().setProperty(getPropKey(setting.lab.code), encodeSetting(setting)) 22 | 23 | 24 | function getDefaults() { 25 | const userPropStore = PropertiesService.getUserProperties() 26 | const settings = getDefaultSettings() 27 | return patchSettings(settings, userPropStore) 28 | } 29 | 30 | 31 | // Remove a custom category from user prop stores 32 | function removePair(code) { 33 | if (isDefault(code)) return new Error('cannot delete default code') 34 | 35 | PropertiesService.getUserProperties().deleteProperty("cross_" + code) 36 | PropertiesService.getDocumentProperties().deleteProperty("cross_" + code) 37 | } 38 | 39 | 40 | // Return the color of highlighted text 41 | function cloneColor(type='lab') { 42 | const selection = DocumentApp.getActiveDocument().getSelection() 43 | if (!selection) { 44 | DocumentApp.getUi().alert( 45 | "Clone colour", "Please select some text with the colour you want to clone.", 46 | DocumentApp.getUi().ButtonSet.OK 47 | ) 48 | return 49 | } 50 | 51 | const element = selection.getRangeElements()[0] 52 | if (!element.getElement().editAsText) return 53 | 54 | const text = element.getElement().editAsText() 55 | const offset = element.isPartial() ? element.getStartOffset() : 0 56 | 57 | return text.getAttributes(offset).FOREGROUND_COLOR 58 | } 59 | -------------------------------------------------------------------------------- /test.gs: -------------------------------------------------------------------------------- 1 | const It = (description, got, want) => 2 | Logger.log( 3 | areDeepEqual(got, want) 4 | ? '✅ ' + description 5 | : '❌ ' + description + '.\nExpected\n' + JSON.stringify(want) + '\n Got\n' + JSON.stringify(got) 6 | ) 7 | 8 | const isObject = thing => Object.prototype.toString.call(thing) === '[object Object]' 9 | const isIterable = thing => isObject(thing) || Array.isArray(thing) // Symbol.iterator approach does not work in gs. No need for sets or maps yet 10 | 11 | 12 | // areSameType returns true if two things are the same type. 13 | // Only supports primitives, objects and arrays 14 | function areSameType(a, b) { 15 | if (isObject(a)) { 16 | return isObject(b) 17 | } 18 | if (Array.isArray(a)) { 19 | return Array.isArray(b) 20 | } 21 | return typeof a === typeof b 22 | } 23 | 24 | 25 | // areSameLength returns true if two items are the same length 26 | // This function is liberal: an array can be the same length as a string; 27 | // an object has a length (the count of its iterable properties) 28 | const areSameLength = (a, b) => { 29 | const lenA = isObject(a) ? Object.keys(a).length : a.length 30 | const lenB = isObject(b) ? Object.keys(b).length : b.length 31 | 32 | return lenA === lenB 33 | } 34 | 35 | 36 | // areDeepEqual returns true if two inputs are deeply equal. 37 | // It only drills down into array and object iterable types 38 | const areDeepEqual = (a, b, strict=true) => { 39 | if (strict && !areSameType(a, b)) { 40 | return false 41 | } 42 | 43 | if (isIterable(a)) { 44 | if (!isIterable(b) || !areSameLength(a, b)) { 45 | return false 46 | } 47 | for (const key in a) { 48 | if (!areDeepEqual(a[key], b[key], strict)) { 49 | return false 50 | } 51 | } 52 | } else { 53 | return string ? a === b : a == b 54 | } 55 | 56 | return true 57 | } 58 | 59 | 60 | function testSuite(name, suite = []) { 61 | Logger.log('SUITE: ' + name) 62 | Logger.log('======') 63 | 64 | suite.forEach((s) => { 65 | Logger.log(s.name) 66 | Logger.log('------') 67 | s() 68 | Logger.log('') 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /main.gs: -------------------------------------------------------------------------------- 1 | function onInstall(e) { 2 | onOpen(e) 3 | } 4 | 5 | function onOpen(e) { 6 | DocumentApp.getUi() 7 | .createAddonMenu() 8 | .addItem('Update document', 'updateDoc') 9 | .addItem('Configure references', 'showSidebar') 10 | .addSeparator() 11 | .addItem('Update lists', 'createLoF') 12 | .addItem('Configure lists', 'showLofConfig') 13 | .addToUi() 14 | } 15 | 16 | function include(filename) { 17 | return HtmlService.createHtmlOutputFromFile(filename).getContent() 18 | } 19 | 20 | function showLofConfig() { 21 | const html = HtmlService.createTemplateFromFile('lof-config').evaluate() 22 | html.setWidth(280).setHeight(180) 23 | DocumentApp.getUi().showModalDialog(html, 'Configure entity lists') 24 | } 25 | 26 | function showSidebar() { 27 | const sidebar = HtmlService.createTemplateFromFile('sidebar-html').evaluate() 28 | sidebar.setTitle('Cross Reference') 29 | DocumentApp.getUi().showSidebar(sidebar) 30 | } 31 | 32 | function updateDoc() { 33 | const document = DocumentApp.getActiveDocument() 34 | const paragraphs = document.getBody().getParagraphs() 35 | const footnotes = document.getFootnotes() 36 | 37 | const settings = getSettings() 38 | updateDocProps(settings) 39 | 40 | const recordedNumbers = {} 41 | const labelNameNumberMap = {} 42 | 43 | const labProps = getProps('lab')(settings) 44 | 45 | const getLabs = getCRUrls(isCRUrl(5)) 46 | const handleNumbering = handleLabNumber(recordedNumbers)(labelNameNumberMap) 47 | const handleLabs = handleCRUrl(labProps)(handleNumbering) 48 | let error = updateParagraphs(paragraphs)(getLabs)(handleLabs) 49 | if (error) { 50 | handleErr(error) 51 | return 'error' 52 | } 53 | 54 | for (let i = 0, len = footnotes.length; i < len; i++) { 55 | const footnoteParagraphs = footnotes[i].getFootnoteContents().getParagraphs() 56 | const handleFNLabs = handleFootnoteLabCRUrl(labProps)(handleNumbering) 57 | const error = updateParagraphs(footnoteParagraphs)(getLabs)(handleFNLabs) 58 | if (error) { 59 | handleErr(error) 60 | return 'error' 61 | } 62 | } 63 | 64 | const refProps = getProps('ref')(settings) 65 | 66 | const getRefs = getCRUrls(isCRUrl(3)) 67 | const handleRefs = handleCRUrl(refProps)(handleRefNumber(recordedNumbers)) 68 | error = updateParagraphs(paragraphs)(getRefs)(handleRefs) 69 | if (error) { 70 | handleErr(error) 71 | return 'error' 72 | } 73 | } 74 | 75 | /** ALL TESTS */ 76 | 77 | function runAllTests() { 78 | testAllSettings() 79 | testAllText() 80 | } 81 | -------------------------------------------------------------------------------- /manual_tests.md: -------------------------------------------------------------------------------- 1 | # In text 2 | 3 | * [x] Default entities are numbered sequentially 4 | * [x] Custom entities are numbered sequentially 5 | * [x] Reference numbering aligns with labels 6 | * [x] Labels are formatted correctly 7 | * [x] Style is correct 8 | * [x] References are formatted correctly 9 | * [x] Style is correct 10 | * [x] Capitalisation is preserved 11 | * [x] All of the above for footnotes 12 | 13 | 14 | # Sidebar 15 | 16 | ## Main dropdown 17 | 18 | * [x] Default categories do not have delete option 19 | * [x] Custom categories do have delete option 20 | * [x] All categories have add option 21 | * [x] Category names match displayed data 22 | * [x] Options are ordered alphabetically on reopen config, even when custom is added 23 | 24 | ## Style panels 25 | 26 | * [x] Values reset when switching categories 27 | 28 | ### Lab 29 | 30 | * [x] Text matches what is displayed in preview 31 | * [x] All style options match preview 32 | * [x] Changes are reflected in preview 33 | * [x] Clone color works 34 | * [x] Clone color defaults to 000000 when cloning uncolored 35 | 36 | ### Ref 37 | 38 | * [x] Text matches what is displayed in preview 39 | * [x] All style options match preview 40 | * [x] Changes are reflected in preview 41 | * [x] Clone color works 42 | * [x] Clone color defaults to 000000 when cloning uncolored 43 | 44 | ### Additional for custom screen 45 | 46 | * [x] Ref codes already taken are flagged and entry is prohibited 47 | * [x] Ref codes less than 5 chars are flagged and save is disabled 48 | * [x] Updating lab code automatically populates ref code 49 | 50 | 51 | ## Adding custom 52 | 53 | * [x] Is preserved through closing and opening sidebar 54 | * [x] Works with updating document through menu 55 | * [x] Shows delete option in menu bar 56 | * [x] Cannot create duplicate ref codes 57 | 58 | ## Deleting custom 59 | 60 | * [x] Not present in top menu 61 | * [x] Not present in temp settings 62 | * [x] Not present in doc props 63 | 64 | ## Defaults 65 | 66 | * [x] Saved defaults can be restored after changes in sidebar 67 | 68 | # Properties 69 | 70 | * [x] Legacy properties are correctly applied to document (requires setting props manually) 71 | * [x] Legacy properties are correctly reflected in sidebar 72 | * [x] Legacy properties are replaced with correct new format properties 73 | 74 | # Errors 75 | 76 | * [x] Duplicate labels produce error on update 77 | * [x] Unrecognised labels produce error on update 78 | * [x] Dangling references are produce error on update 79 | 80 | # List of Figures 81 | 82 | * [x] All figures included 83 | * [x] Numbers match 84 | * [x] Pages match 85 | * [x] Positioned at top of document 86 | -------------------------------------------------------------------------------- /lof-config.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 28 | 29 | 30 |
31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 |
39 |
40 | 64 | 65 | 66 | 96 | 97 | -------------------------------------------------------------------------------- /settings.test.gs: -------------------------------------------------------------------------------- 1 | function testAllSettings() { 2 | testSuite('settings.js', [ 3 | testEncodeSettings, 4 | testGetPropKey, 5 | testIsCrossProp, 6 | testIsLegacy, 7 | testLegacySettingsMapsToNewSettings, 8 | testPatchSettings, 9 | ]) 10 | } 11 | 12 | function testLegacySettingsMapsToNewSettings() { 13 | let legacy = 'figur_Figure_Fig. _true_null_null_figure _null_true_null_null_555555' 14 | let want = { 15 | name: 'Figure', 16 | lab: { 17 | code: 'figur', 18 | text: 'Fig. ', 19 | isBold: true, 20 | isItalic: false, 21 | isUnderlined: false, 22 | color: null, 23 | suffix: '', 24 | }, 25 | ref: { 26 | code: 'fig', 27 | text: 'figure ', 28 | isBold: false, 29 | isItalic: true, 30 | isUnderlined: false, 31 | color: '555555', 32 | suffix: '', 33 | } 34 | } 35 | It('returns correctly formatted new settings for legacy string', 36 | decodeLegacy(legacy), 37 | want 38 | ) 39 | } 40 | 41 | function testIsLegacy() { 42 | It('returns false for JSON', 43 | isLegacy(JSON.stringify({ a: 'hello' })), 44 | false 45 | ) 46 | It('returns true for non-JSON', 47 | isLegacy('hello'), 48 | true 49 | ) 50 | } 51 | 52 | function testPatchSettings() { 53 | let settings = getDefaultSettings() 54 | let storedProps = { 55 | 'cross_fig': encodeSetting({ 56 | name: 'testName', 57 | lab: { 'code': 'figur' }, 58 | }) 59 | } 60 | 61 | It('replaces "figur" entry with one in stored props', 62 | patchSettings(settings, storedProps)['figur'], 63 | decodeSetting(storedProps['cross_fig']) 64 | ) 65 | 66 | settings = getDefaultSettings() 67 | storedProps = { 68 | 'cross_tig': encodeSetting({ 69 | name: 'Tiger', 70 | lab: { 'code': 'tiger' }, 71 | }) 72 | } 73 | It('adds entry if not present', 74 | patchSettings(settings, storedProps)['tiger'], 75 | decodeSetting(storedProps['cross_tig']) 76 | ) 77 | 78 | settings = getDefaultSettings() 79 | const originalLength = Object.keys(settings).length 80 | It('does not overwrite existing', 81 | Object.keys(patchSettings(settings, storedProps)).length, 82 | originalLength + 1 83 | ) 84 | } 85 | 86 | function testEncodeSettings() { 87 | let settings = { figur: getDefaultSettings().figur } 88 | 89 | It('encodes such that the decoded result is the same as original', 90 | decodeSetting(encodeSettings(settings).cross_fig), 91 | settings.figur 92 | ) 93 | 94 | settings = getDefaultSettings() 95 | It('encoded object is correct length', 96 | Object.keys(encodeSettings(settings)).length, 97 | Object.keys(settings).length 98 | ) 99 | } 100 | 101 | function testIsCrossProp() { 102 | It('crossprop returns true', 103 | isCrossProp('cross_fig'), 104 | true 105 | ) 106 | It('non-cross prop returns false', 107 | isCrossProp('something'), 108 | false 109 | ) 110 | } 111 | 112 | function testGetPropKey() { 113 | It('formats correctly', 114 | getPropKey('figur'), 115 | 'cross_fig' 116 | ) 117 | } 118 | 119 | 120 | function testGetProps() { 121 | const settings = {figur: getDefaultSettings().figur} 122 | let got = getProps('lab', settings) 123 | 124 | It('returns correct key', 125 | Object.keys(got)[0], 126 | settings.figur.lab.code, 127 | ) 128 | It('returns correct value', 129 | got[settings.figur.lab.code], 130 | settings.figur.lab, 131 | ) 132 | } 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ⚠️ This add on is not longer actively maintained. I don't have time to fix bugs or add features unless they completely break the add on. 2 | 3 | 4 | 5 | Cross Reference is an open source cross referencing add-on for Google Docs that automatically numbers elements such as figures and updates and formats in-text references to them. [Install](https://chrome.google.com/webstore/detail/cross-reference/hknkaiempgninehdhkgekoeoilkapgob?hl=en-GB). 6 | 7 | ## Short instructions 8 | 9 | Create a label for a figure by writing the word 'figure' in the caption and then adding a hyperlink 10 | to that word with the url `#figur_YOURCHOSENNAME`. Refer to this figure in your text by writing 'figure' again and 11 | adding a hyperlink with `#fig_YOURCHOSENNAME`. Update the document via 'Add-ons->Cross Reference->Update document'. 12 | 13 | ## More detailed instructions 14 | 15 | Cross Reference is built around two concepts: labels and references. 16 | 17 | A label is always unique and is associated with a single figure (or table etc.); this is usually represented by text such as _figure 1_ in the figure caption. 18 | 19 | A reference is usually in the text, and refers to a labelled figure (or table etc.). There can be many references to a single labelled entity. 20 | 21 | To refer to, say, a figure you must create a label for it, usually in the caption. Cross Reference uses 22 | hyperlinks to create labels. These have the following syntax: `#figur_population`. `#figur` is the built-in prefix used by Cross Reference; `population` in this case is your chosen name for the figure. Notice that the prefix uses `figur` rather than `figure` with an `e` -- all prefixes in Cross Reference are composed of a hash and a five-letter code, and separated from the name with an underscore. 23 | 24 | To create this label, you simply need to create some dummy text and add the link. You don't need to add any numbering, as 25 | this is taken care of by Cross Reference. 26 | So in the caption you could add the word 'figure', highlight that word, goto 'Tools->Insert link' and add `#figur_population` as 27 | the URL. (The dummy text will replaced by the add-on, so don't highlight text that you want to keep, such as other parts 28 | of the caption.) 29 | 30 | You now have a label that can be referred to. Creating references is similar to creating labels. First, create some dummy 31 | text, such as the word 'figure' -- so you might write something like "for an example of this phenomenon, see figure". Again, 32 | you don't need any numbering, as the add-on will take care of this. Now you need to apply another hyperlink, in this case to the word 'figure'. Reference syntax, however, is slightly 33 | different; in our case, it will be `#fig_population`. This must be identical to the label hyperlink, except that the code is now `fig` not `figur` (reference codes always use the first three letters of the code used for labels.) 34 | 35 | If you now goto the 'Add-ons->Cross Reference->Update document', your text should be replaced with a numbered label and 36 | a reference with the same number. 37 | 38 | Because Cross Reference replaces your dummy text, you can use a short word or even one letter, and you can also use the `Ctrl+K` shortcut to add the link. These two additions to the process should make things quicker. 39 | 40 | ## Not working? 41 | 42 | Aside from bugs, the biggest source of user error is using label syntax for references or vice-versa. Labels and references 43 | use a different code. If your references have nothing to refer to, make sure you created a label under you figure, not a 44 | reference. 45 | 46 | # This didn't work / I don't get it / how do I change the style of labels or references? 47 | 48 | The wiki covers the use of Cross Reference in more depth, including customising labels and references. Please feel free to raise issues or ask questions! 49 | 50 | [WIKI](https://github.com/davidrthorn/cross_reference/wiki/Cross-Reference-for-Google-Docs) 51 | -------------------------------------------------------------------------------- /text.test.gs: -------------------------------------------------------------------------------- 1 | function testAllText() { 2 | testSuite('text.js', [ 3 | testGetStyle, 4 | testCodeFromCRUrl, 5 | testCapitalizeIfAppropriate, 6 | testIsCapitalized, 7 | testCapitalize, 8 | testLabelNumberHandler, 9 | testRefNumberHandler, 10 | ]) 11 | } 12 | 13 | function testCodeFromCRUrl() { 14 | It('ref url returns 3-string code', 15 | codeFromUrl('#fig_hello'), 16 | 'fig' 17 | ) 18 | It('lab url returns 5 string code', 19 | codeFromUrl('#figur_hello'), 20 | 'figur' 21 | ) 22 | It('returns null for other url', 23 | codeFromUrl('https://google.com'), 24 | null 25 | ) 26 | It('returns null for malformed CRUrl', 27 | codeFromUrl('#figu_hello'), 28 | null 29 | ) 30 | } 31 | 32 | function testCapitalizeIfAppropriate() { 33 | It('returns capitalized when reference text is already capitalized', 34 | capitalizeIfAppropriate('aa bb Figure', 5, 'Figure'), 35 | 'Figure' 36 | ) 37 | It('returns capitalized when text to replace is already capitalized', 38 | capitalizeIfAppropriate('aa bb Figur', 5, 'figure'), 39 | 'Figure' 40 | ) 41 | It('returns uncapitalized when text to replace is not capitalized', 42 | capitalizeIfAppropriate('aa bb figur', 5, 'figure'), 43 | 'Figure' 44 | ) 45 | } 46 | 47 | function testCapitalize() { 48 | It('returns empty for empty', 49 | capitalize(''), 50 | '' 51 | ) 52 | It('capitalizes non-empty string', 53 | capitalize('hello'), 54 | 'Hello' 55 | ) 56 | It('does not modify already capitalized', 57 | capitalize('Hello'), 58 | 'Hello' 59 | ) 60 | } 61 | 62 | function testIsCapitalized() { 63 | It('returns false for empty string', 64 | isCapitalized(''), 65 | false 66 | ) 67 | It('returns true if is capitalized', 68 | isCapitalized('Hello'), 69 | true 70 | ) 71 | It('returns false if not capitalize', 72 | isCapitalized('hello'), 73 | false 74 | ) 75 | } 76 | 77 | function testLabelNumberHandler() { 78 | const recordedNumbers = { 79 | '#fig_first': 1, 80 | } 81 | const countByLabelType = { 82 | 'figur': 1, 83 | 'table': 2, 84 | } 85 | let sut = handleLabNumber(recordedNumbers)(countByLabelType) 86 | 87 | let got = sut('#figur_somename') 88 | 89 | It('increments the existing entry in the label map', 90 | countByLabelType['figur'], 91 | 2 92 | ) 93 | It('records the url in the recorded numbers', 94 | recordedNumbers, 95 | { 96 | '#fig_first': 1, 97 | '#fig_somename': 2, 98 | } 99 | ) 100 | It('returns the correct number', 101 | got, 102 | 2 103 | ) 104 | 105 | got = sut('#figur_first') 106 | It('returns duplicate error', 107 | sut('#figur_first').message, 108 | 'duplicate' 109 | ) 110 | } 111 | 112 | 113 | function testRefNumberHandler() { 114 | let recordedNumbers = { 115 | '#fig_first': 1, 116 | '#fig_second': 2, 117 | } 118 | let sut = handleRefNumber(recordedNumbers) 119 | 120 | let got = sut('#fig_second') 121 | It('returns correct number for url', 122 | got, 123 | 2 124 | ) 125 | It('returns error for missing url', 126 | sut('#fig_missing').message, 127 | 'missref' 128 | ) 129 | } 130 | 131 | 132 | function testGetStyle() { 133 | let settings = getDefaultSettings() 134 | let props = getProps('lab')(settings) 135 | 136 | let got = getStyle(props.figur) 137 | It('returns correct style object for default', 138 | got, 139 | { BOLD: false, ITALIC: false, UNDERLINE: false, FOREGROUND_COLOR: null } 140 | ) 141 | } 142 | 143 | 144 | function testGetCRUrls() { 145 | let mockText = { 146 | getText: () => 'lorem ipsum', 147 | getTextAttributeIndices: () => [6, 10], 148 | getLinkUrl: idx => [6, 9].includes(idx) ? '#figur_test' : null 149 | } 150 | const isCR = isCRUrl(5) 151 | 152 | It('returns start and url', 153 | getCRUrls(isCR)(mockText), 154 | [{ start: 6, end: 9, url: '#figur_test'}] 155 | ) 156 | 157 | mockText.getLinkUrl = idx => [6, 9].includes(idx) ? '#figur_test' : 'https://google.com' 158 | It("doesn't get broken by surrounding links", 159 | getCRUrls(isCR)(mockText), 160 | [{ start: 6, end: 9, url: '#figur_test'}] 161 | ) 162 | 163 | } 164 | 165 | 166 | function testUpdateText() { 167 | const results = [] 168 | let got = updateText([1, 2, 3])(cr => {results.push(cr)}) 169 | 170 | It('calls mock handler for each cr in reverse order', 171 | results, 172 | [3, 2, 1] 173 | ) 174 | It('returns undefined if no error', 175 | got, 176 | undefined 177 | ) 178 | 179 | got = updateText([1, 2, 3])(cr => new Error('hello')) 180 | It('returns error if there is one', 181 | typeof got.message, 182 | 'string' 183 | ) 184 | } 185 | -------------------------------------------------------------------------------- /sidebar-style.html: -------------------------------------------------------------------------------- 1 | 234 | -------------------------------------------------------------------------------- /text.gs: -------------------------------------------------------------------------------- 1 | const isCRUrl = codeLength => url => (new RegExp('^#[^_]{' + codeLength + '}_')).test(url) 2 | 3 | const handleRefNumber = numberForRefUrl => url => numberForRefUrl[url] || new Error('missref') 4 | 5 | const isCapitalized = str => str !== '' && str.charAt(0) === str.charAt(0).toUpperCase() 6 | 7 | const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1) 8 | 9 | 10 | function codeFromUrl(url) { 11 | if (!url) return 12 | const match = url.match(/^#([^_]{3}|[^_]{5})_/) 13 | return match ? match[1] : null 14 | } 15 | 16 | 17 | /* 18 | @var numForRefUrl : {} -- map of `ref url` to `number for the corresponding label` 19 | @var countByLabelType : {} -- map of `label code` to `count of code in document` 20 | @var url : string -- the current url 21 | @return int | error -- the number for the current label 22 | */ 23 | const handleLabNumber = numberForRefUrl => countByLabelType => url => { 24 | const code = codeFromUrl(url) 25 | const refEquivalent = '#' + code.substr(0, 3) + '_' + url.substr(7) 26 | 27 | const num = countByLabelType[code] + 1 || 1 28 | 29 | if (refEquivalent in numberForRefUrl) return new Error('duplicate') 30 | 31 | numberForRefUrl[refEquivalent] = num 32 | countByLabelType[code] = num 33 | 34 | return num 35 | } 36 | 37 | 38 | /* 39 | @var CRUrls : [{start, end, url}] 40 | @var handleCR : {start, end, url} -> ?error 41 | */ 42 | const updateText = CRUrls => handleCR => { 43 | let i = CRUrls.length 44 | while (i--) { // iterate backwards because we're changing the underlying text length 45 | const error = handleCR(CRUrls[i]) 46 | if (error) { 47 | return error 48 | } 49 | } 50 | } 51 | 52 | /* 53 | @var paragraphs : [paragraphs] 54 | @var getCRs : text -> [{start, end, url}] 55 | @var handleText : text -> [{start, end, url}] -> ?error 56 | @return ?error 57 | */ 58 | const updateParagraphs = paragraphs => getCRs => handleText => { 59 | for (let i = 0, len = paragraphs.length; i < len; i++) { 60 | const text = paragraphs[i].editAsText() 61 | 62 | const CRUrls = getCRs(text) 63 | if (!CRUrls.length) continue 64 | 65 | const handleCR = handleText(text) 66 | const error = updateText(CRUrls)(handleCR) 67 | if (error) { 68 | return error 69 | } 70 | } 71 | } 72 | 73 | 74 | const replaceText = (text, CRUrl, replacementText, style) => { 75 | const { start, end, url } = CRUrl 76 | const replacementEnd = start + replacementText.length - 1 77 | const size = text.getFontSize(start) 78 | 79 | text.deleteText(start, end) 80 | .insertText(start, replacementText) 81 | .setLinkUrl(start, replacementEnd, url) 82 | .setAttributes(start, replacementEnd, style) 83 | .setFontSize(start, replacementEnd, size) 84 | } 85 | 86 | 87 | const getStyle = prop => ({ 88 | 'BOLD': prop.isBold, 89 | 'ITALIC': prop.isItalic, 90 | 'UNDERLINE': prop.isUnderlined, 91 | 'FOREGROUND_COLOR': prop.color, 92 | }) 93 | 94 | /* 95 | @var props : {} -- map of `code` to `properties` 96 | @var handleNumbering : {start, end, url} -> int 97 | @var text : Text 98 | @var CRUrl : {start, end, url} 99 | @return ?error 100 | */ 101 | const handleCRUrl = props => handleNumbering => text => CRUrl => { 102 | const foundCode = codeFromUrl(CRUrl.url) 103 | 104 | const prop = props[foundCode] 105 | if (!prop) { 106 | return new CRError(text, CRUrl, 'unrecognised') 107 | } 108 | 109 | let replacementText = capitalizeIfAppropriate(text.getText(), CRUrl.start, prop.text) 110 | const num = handleNumbering(CRUrl.url) 111 | if (num instanceof Error) { 112 | return new CRError(text, CRUrl, num.message) 113 | } 114 | 115 | replacementText += num + prop.suffix 116 | replacementText = replacementText.replace(' ', '\xA0') 117 | 118 | const style = getStyle(prop) 119 | 120 | replaceText(text, CRUrl, replacementText, style) 121 | } 122 | 123 | 124 | /* 125 | @var props : {} -- map of `code` to `properties` 126 | @var handleNumbering : {start, end, url} -> int 127 | @var text : Text 128 | @var CRUrl : {start, end, url} 129 | @return ?error 130 | */ 131 | const handleFootnoteLabCRUrl = props => handleNumbering => text => CRUrl => { 132 | const foundCode = codeFromUrl(CRUrl.url) 133 | if (foundCode !== 'fnote') return 134 | 135 | const num = handleNumbering(CRUrl.url) 136 | if (num instanceof Error) { 137 | return new CRError(text, CRUrl, num.message) 138 | } 139 | 140 | text.setAttributes(CRUrl.start, CRUrl.end, {'UNDERLINE': null, 'FOREGROUND_COLOR': '#000000'}) 141 | } 142 | 143 | 144 | /* 145 | @var isCRUrl : string -> bool 146 | @var text : Text 147 | @return [{start, end, url}] 148 | */ 149 | const getCRUrls = isCRUrl => text => { 150 | const textLength = text.getText().length 151 | const idxs = text.getTextAttributeIndices() 152 | idxs.push(textLength) // the final index is the end of the text 153 | 154 | const CRUrls = [] 155 | 156 | for (let i = 0, len = idxs.length; i < len; i++) { 157 | const idx = idxs[i] 158 | 159 | const urlHere = idx !== textLength ? text.getLinkUrl(idx) : null 160 | const urlToTheLeft = i > 0 ? text.getLinkUrl(idx - 1) : null 161 | 162 | const isStart = !isCRUrl(urlToTheLeft) && isCRUrl(urlHere) 163 | const isEnd = isCRUrl(urlToTheLeft) && !isCRUrl(urlHere) 164 | 165 | if (isStart) { 166 | CRUrls.push({ start: idx, url: urlHere }) 167 | } 168 | if (isEnd) { 169 | CRUrls[CRUrls.length - 1].end = idx - 1 170 | } 171 | } 172 | return CRUrls 173 | } 174 | 175 | 176 | const capitalizeIfAppropriate = (text, start, replacementText) => 177 | isCapitalized(replacementText) || !isCapitalized(text.substr(start, start + 1)) 178 | ? replacementText 179 | : capitalize(replacementText) 180 | -------------------------------------------------------------------------------- /settings.gs: -------------------------------------------------------------------------------- 1 | // isCrossProp returns true if a string is a key from the gDocs property store 2 | const isCrossProp = propKey => propKey.substr(0, 6) === 'cross_' 3 | 4 | // getPropKey returns a key to be used in the gDocs property stores 5 | const getPropKey = labCode => 'cross_' + labCode.substr(0, 3) 6 | 7 | const refCodeFrom = labCode => labCode.substr(0, 3) 8 | 9 | const encodeSetting = s => JSON.stringify(s) 10 | 11 | const decodeSetting = s => JSON.parse(s) 12 | 13 | const isDefault = code => ['equ', 'fig', 'fno', 'tab'].includes(code.substr(0, 3)) 14 | 15 | 16 | // encodeSettings returns an object where key is the key to be used 17 | // in the gDocs prop stores and value is the settings encoded as a string 18 | function encodeSettings(settings) { 19 | const result = {} 20 | for (const key in settings) { 21 | const setting = settings[key] 22 | result[getPropKey(setting.lab.code)] = encodeSetting(setting) 23 | } 24 | return result 25 | } 26 | 27 | 28 | // getSettings retrieves a combination of default, user-level and document-level 29 | // settings (in that order of priority) 30 | function getSettings() { 31 | let settings = getDefaultSettings() 32 | settings = patchSettings(settings, PropertiesService.getUserProperties()) 33 | settings = patchSettings(settings, PropertiesService.getDocumentProperties()) 34 | 35 | return settings 36 | } 37 | 38 | 39 | // patchSettings overwrites settings with those from a given gDocs property store 40 | function patchSettings(settings, propStore) { 41 | const props = propStore.getProperties() 42 | 43 | for (const key in props) { 44 | if (!isCrossProp(key)) continue 45 | 46 | const encoded = props[key] 47 | 48 | let s = null 49 | if (isLegacy(encoded)) { 50 | s = decodeLegacy(encoded) 51 | propStore.setProperty(key, encodeSetting(s)) // update legacy props 52 | } else { 53 | s = decodeSetting(encoded) 54 | } 55 | 56 | settings[s.lab.code] = s 57 | } 58 | return settings 59 | } 60 | 61 | 62 | // getProps returns the sub-settings for a particular type of cross reference 63 | // (e.g. label or reference) with codes as keys (e.g. {fig: {...}}, or {figur: {...}}) 64 | const getProps = type => settings => { 65 | const props = {} 66 | for (const key in settings) { 67 | const setting = settings[key] 68 | props[setting[type].code] = setting[type] 69 | } 70 | return props 71 | } 72 | 73 | 74 | function clearPropStore(store) { 75 | for (const key in store) { 76 | if (isCrossProp(key)) { 77 | store.deleteProperty(key) 78 | } 79 | } 80 | } 81 | 82 | 83 | function updateDocProps() { 84 | const encoded = encodeSettings(getSettings()) 85 | PropertiesService.getDocumentProperties().setProperties(encoded) 86 | } 87 | 88 | 89 | function getDefaultSettings() { 90 | return { 91 | equat: { 92 | name: 'Equation', 93 | lab: { 94 | code: 'equat', 95 | text: 'equation ', 96 | isBold: false, 97 | isItalic: false, 98 | isUnderlined: false, 99 | color: null, 100 | suffix: '', 101 | }, 102 | ref: { 103 | code: 'equ', 104 | text: 'equation ', 105 | isBold: false, 106 | isItalic: false, 107 | isUnderlined: false, 108 | color: null, 109 | suffix: '', 110 | } 111 | }, 112 | figur: { 113 | name: 'Figure', 114 | lab: { 115 | code: 'figur', 116 | text: 'figure ', 117 | isBold: false, 118 | isItalic: false, 119 | isUnderlined: false, 120 | color: null, 121 | suffix: '', 122 | }, 123 | ref: { 124 | code: 'fig', 125 | text: 'figure ', 126 | isBold: false, 127 | isItalic: false, 128 | isUnderlined: false, 129 | color: null, 130 | suffix: '', 131 | } 132 | }, 133 | fnote: { 134 | name: 'Footnote', 135 | lab: { 136 | code: 'fnote', 137 | text: '', 138 | isBold: false, 139 | isItalic: false, 140 | isUnderlined: false, 141 | color: null, 142 | suffix: '', 143 | }, 144 | ref: { 145 | code: 'fno', 146 | text: 'fn. ', 147 | isBold: false, 148 | isItalic: false, 149 | isUnderlined: false, 150 | color: null, 151 | suffix: '', 152 | }, 153 | }, 154 | table: { 155 | name: 'Table', 156 | lab: { 157 | code: 'table', 158 | text: 'table ', 159 | isBold: false, 160 | isItalic: false, 161 | isUnderlined: false, 162 | color: null, 163 | suffix: '', 164 | }, 165 | ref: { 166 | code: 'tab', 167 | text: 'table ', 168 | isBold: false, 169 | isItalic: false, 170 | isUnderlined: false, 171 | color: null, 172 | suffix: '', 173 | } 174 | } 175 | } 176 | } 177 | 178 | /* Required for legacy. Hardcoded as hell */ 179 | const isLegacy = encoded => encoded.charAt(0) !== '{' 180 | 181 | const decodeLegacy = encoded => { 182 | const asArr = encoded.split('_') 183 | const bool = str => str === 'true' 184 | const realNull = str => str === 'null' ? null : str 185 | 186 | return { 187 | name: asArr[1], 188 | lab: { 189 | code: asArr[0], 190 | text: asArr[2], 191 | isBold: bool(asArr[3]), 192 | isItalic: bool(asArr[4]), 193 | isUnderlined: bool(asArr[5]), 194 | color: realNull(asArr[10]), 195 | suffix: '', 196 | }, 197 | ref: { 198 | code: refCodeFrom(asArr[0]), 199 | text: asArr[6], 200 | isBold: bool(asArr[7]), 201 | isItalic: bool(asArr[8]), 202 | isUnderlined: bool(asArr[9]), 203 | color: realNull(asArr[11]), 204 | suffix: '', 205 | } 206 | } 207 | } 208 | 209 | /* 210 | Legacy properties example: 211 | 212 | */ 213 | 214 | /** For debugging purposes */ 215 | 216 | function setLegacyDocProps() { 217 | clearProps() 218 | PropertiesService.getDocumentProperties().setProperties({ 219 | cross_tab: 'table_Table_table _null_null_null_table _null_null_null_null_null', 220 | cross_tes: 'testi_Testing_testing _null_null_null_testing _null_null_null_null_null', 221 | cross_fig: 'figur_Figure_figFIGFIG _null_null_null_figure _null_null_null_null_null', 222 | cross_equ: 'equat_Equation_equation _null_null_null_equation _null_null_null_null_null', 223 | cross_fno: 'fnote_Footnote__null_null_null_fn. _null_null_null_null_null' 224 | }) 225 | } 226 | 227 | function clearProps() { 228 | PropertiesService.getDocumentProperties().deleteAllProperties() 229 | PropertiesService.getUserProperties().deleteAllProperties() 230 | } 231 | 232 | function viewProps() { 233 | Logger.log(PropertiesService.getDocumentProperties().getProperties()) 234 | Logger.log(PropertiesService.getUserProperties().getProperties()) 235 | } 236 | -------------------------------------------------------------------------------- /sidebar-html.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 |
9 |
+
10 |
+
11 | 12 | 13 |
14 | 15 |
16 |
17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 | 28 |
29 |

Add footnote label to any word
within a footnote.

30 |

No footnote text is replaced
or styled.

31 |
32 |
33 |

Labels

34 |
35 |
Code
36 |
37 |
38 |
39 | #  40 | 41 |
42 |
43 |
44 |
45 |
Text
46 |
47 | 48 | 49 |
50 |
51 |
52 |
Style
53 |
54 | 55 | 56 | 57 |
58 |
59 |
60 |
Colour
61 |
62 | 63 | 64 |
65 |
66 |
67 |
68 |
69 |
Figure
70 |
1
71 |
72 |
 Early human migration route out of Africa
73 |
74 |
75 |
76 |
77 | 78 |
79 |

References

80 |
81 |
Code
82 |
#
83 |
84 |
85 |
Text
86 |
87 | 88 | 89 |
90 |
91 |
92 |
Style
93 |
94 | 95 | 96 | 97 |
98 |
99 |
100 |
Colour
101 |
102 | 103 | 104 |
105 |
106 |
107 |
108 |
109 |
the line in
110 |
figure
111 |
1
112 |
113 |
 represents the path of human
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 | 122 | 123 |
124 |
125 | 126 | 127 |
128 |
129 |
130 | 131 |
132 | 134 | -------------------------------------------------------------------------------- /lof-code.gs: -------------------------------------------------------------------------------- 1 | const isFigLab = url => /^#figur_/.test(url) 2 | 3 | const getDocAsPDF = () => DocumentApp.getActiveDocument().getBlob().getBytes() 4 | 5 | const saveLofConfig = settings => PropertiesService.getDocumentProperties().setProperty('entityTablesSettings', JSON.stringify(settings)) 6 | 7 | const getLofConfig = () => JSON.parse(PropertiesService.getDocumentProperties().getProperty('entityTablesSettings')) 8 | 9 | 10 | function createLoF() { 11 | if (updateDoc() === 'error') return 12 | 13 | const settings = getLofConfig() || {'fig': {'order': 0}} 14 | const codes = Object.keys(settings).sort((a, b) => settings[a].order || 0 - settings[b].order || 0) 15 | 16 | const signs = filterObjectByKeys({ 17 | 'fig': '☙', 'tab': '❆' 18 | }, codes) 19 | 20 | const positions = getPositions() 21 | 22 | const descriptions = encodeLabel(signs) 23 | 24 | if (!descriptions.fig.length && !descriptions.tab.length) return 25 | 26 | let fallbackPosition = getCursorParagraphIndex() || 0 // must assign this to variable before deleting LoFs 27 | for (const i in codes) { 28 | const code = codes[i] 29 | if (descriptions[code].length === 0) continue 30 | insertDummyLoF(code, descriptions[code], positions[i] || fallbackPosition++) 31 | } 32 | 33 | const html = HtmlService.createTemplateFromFile('lof').evaluate() 34 | html.setWidth(250).setHeight(90) 35 | DocumentApp.getUi().showModalDialog(html, 'Generating lists') 36 | } 37 | 38 | 39 | const getElementIndex = el => el.getParent().getChildIndex(el) 40 | 41 | 42 | function getPositions() { 43 | const figTable = findLoF('fig') 44 | const tabTable = findLoF('tab') 45 | 46 | const positions = [] 47 | if (figTable) { 48 | positions.push(getElementIndex(figTable)) 49 | } 50 | if (tabTable) { 51 | positions.push(getElementIndex(tabTable)) 52 | } 53 | 54 | figTable && figTable.removeFromParent() 55 | tabTable && tabTable.removeFromParent() 56 | 57 | return positions.sort() 58 | } 59 | 60 | function filterObjectByKeys(object, keys) { 61 | const result = {} 62 | for (const k in object) { 63 | if (keys.includes(k)) { 64 | result[k] = object[k] 65 | } 66 | } 67 | return result 68 | } 69 | 70 | 71 | function getCursorParagraphIndex() { 72 | const cursor = DocumentApp.getActiveDocument().getCursor() 73 | if (!cursor) return 0 74 | const paragraph = getContainingParagraph(cursor.getElement()) 75 | return paragraph.getParent().getChildIndex(paragraph) 76 | } 77 | 78 | 79 | function getContainingParagraph(el) { 80 | if (!el || typeof el.getType !== 'function') return 81 | const type = el.getType() 82 | 83 | if (type === DocumentApp.ElementType.PARAGRAPH) return el 84 | if (type === DocumentApp.ElementType.BODY_SECTION) return 85 | return getContainingParagraph(el.getParent()) 86 | } 87 | 88 | 89 | // encodeLabel replaces the beginning of a label with 90 | // a rare UTF-8 characters that will be used 91 | // to identify labels when we process the PDF file 92 | function encodeLabel(signMap) { 93 | const doc = DocumentApp.getActiveDocument() 94 | const paragraphs = doc.getBody().getParagraphs() 95 | const descriptions = {'fig': [], 'tab':[]} 96 | 97 | const getCRs = getCRUrls(isCRUrl(5)) 98 | const handleText = text => CRUrl => { 99 | const code = CRUrl.url.substr(1, 3) 100 | let sign = signMap[code] 101 | if (!sign) return 102 | 103 | descriptions[code].push(text.getText()) 104 | const start = CRUrl.start 105 | text.deleteText(start + 1, start + 2).insertText(start + 1, sign) 106 | } 107 | 108 | const error = updateParagraphs(paragraphs)(getCRs)(handleText) 109 | 110 | return descriptions 111 | } 112 | 113 | 114 | function deleteLoF(code) { 115 | const lofTable = findLoF(code) 116 | 117 | if (!lofTable) return 118 | 119 | const lofIndex = lofTable.getParent().getChildIndex(lofTable) 120 | // lofTable.removeFromParent() 121 | 122 | return lofIndex 123 | } 124 | 125 | 126 | function findLoF(code) { 127 | const doc = DocumentApp.getActiveDocument() 128 | let lof = doc.getNamedRanges('lofTable_' + code)[0] 129 | 130 | // handle legacy case 131 | if (!lof && code === 'fig') { 132 | lof = DocumentApp.getActiveDocument().getNamedRanges('lofTable')[0] 133 | } 134 | 135 | if (!lof) return 136 | 137 | const el = lof.getRange().getRangeElements()[0] 138 | if (!el) return 139 | 140 | const element = el.getElement() 141 | return getTable(element) || getTable(element.getNextSibling()) // Fallback because swapping order of two tables gets preceding paragraph rather than table (for some reason) 142 | } 143 | 144 | const getTable = element => element && element.getType() === DocumentApp.ElementType.TABLE ? element.asTable() : null 145 | 146 | 147 | function insertDummyLoF(code, descriptions=[], position) { 148 | const doc = DocumentApp.getActiveDocument() 149 | const placeholder = '...' 150 | const lofCells = descriptions.map(fd => [fd.replace(/^[\t\r\n]/, ''), placeholder]) 151 | 152 | const lofTable = doc.getBody().insertTable(position, lofCells) 153 | styleLoF(lofTable) 154 | 155 | const range = doc.newRange() 156 | range.addElement(lofTable) 157 | doc.addNamedRange('lofTable_' + code, range.build()) 158 | } 159 | 160 | 161 | function styleLoF(lofTable) { 162 | const styleAttributes = { 163 | 'BOLD': null, 164 | 'ITALIC': null, 165 | 'UNDERLINE': null, 166 | 'FONT_SIZE': null 167 | } 168 | 169 | lofTable.setBorderWidth(0) 170 | .setAttributes(styleAttributes) 171 | .setColumnWidth(1, 64) 172 | 173 | let i = lofTable.getNumRows() 174 | while (i--) { 175 | const row = lofTable.getRow(i) 176 | 177 | row.getCell(0) 178 | .setPaddingLeft(0) 179 | .setVerticalAlignment(DocumentApp.VerticalAlignment.BOTTOM) 180 | row.getCell(1) 181 | .setPaddingRight(0) 182 | .setVerticalAlignment(DocumentApp.VerticalAlignment.BOTTOM) 183 | .getChild(0).asParagraph() 184 | .setAlignment(DocumentApp.HorizontalAlignment.RIGHT) 185 | } 186 | } 187 | 188 | 189 | /* 190 | @var pageNumbers : [1,0,2...] -- members correspond to count of labels on a page (in order) 191 | */ 192 | function insertLoFNumbers(pageNumbers) { 193 | for (const code of ['fig', 'tab']) { 194 | const lofTable = findLoF(code) 195 | if (!lofTable) continue 196 | 197 | let currentRow = 0 198 | 199 | for (let i = 0; i < pageNumbers[code].length; i++) { 200 | const labCount = pageNumbers[code][i] 201 | if (!labCount) continue 202 | 203 | const pageNumber = (i + 1).toString() 204 | 205 | for (let j = currentRow; j < lofTable.getNumRows(); j++) { 206 | lofTable.getCell(j, 1) 207 | .clear() 208 | .getChild(0).asParagraph() 209 | .appendText(pageNumber) 210 | } 211 | currentRow += labCount 212 | } 213 | } 214 | } 215 | 216 | 217 | function restoreLabels() { 218 | const paragraphs = DocumentApp.getActiveDocument().getBody().getParagraphs() 219 | 220 | const getLabs = getCRUrls(isFigLab) 221 | const handleText = text => CRUrl => { 222 | const start = CRUrl.start 223 | text.deleteText(start + 1, start + 2) 224 | } 225 | 226 | const error = updateParagraphs(paragraphs)(getLabs)(handleText) 227 | 228 | updateDoc() 229 | } 230 | -------------------------------------------------------------------------------- /sidebar-js.html: -------------------------------------------------------------------------------- 1 | 452 | --------------------------------------------------------------------------------