clean-mla-gdoc 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # cleanMLAGDoc.js v1.0.2
2
+
3
+ A lightweight JavaScript library specifically designed to reformat **Google Docs HTML exports** into the **MLA (Modern Language Association) Style** layout. It handles physical pagination, alphabetical sorting of Works Cited, and provides a "DRAFT" watermark feature.
4
+
5
+ > [!IMPORTANT]
6
+ > **Compatibility Note:** This library is specifically tailored for the class structures (`.c1`, `.c7`, `.doc-content`) generated by **Google Docs** when exported as HTML. For best results, ensure your Google Doc is using the **Report** preset before exporting.
7
+
8
+ ## Features
9
+ * **Dynamic Tab Title**: Automatically renames the browser tab to `[LastName] [FirstName] [AssignmentName]`.
10
+ * **Auto-Pagination**: Breaks content into 8.5" x 11" pages with 1-inch margins.
11
+ * **MLA Header**: Processes the 4-line header and title from Google Docs classes.
12
+ * **Smart Date Mapping**: Converts numerical dates (e.g., `12/25/2026`) to MLA format (`25 Dec. 2026`).
13
+ * **Works Cited Sorting**: Automatically alphabetizes the "Works Cited" section.
14
+ * **Draft Mode**: Trigger a "DRAFT" watermark by adding `| draft` to your document header.
15
+
16
+ ## How the Tab Title Works
17
+ The script automatically updates the browser tab title following a professional file-naming convention. It extracts the student's name from the first paragraph and the assignment title from the header. To maintain a professional look, the script **strips the `| draft` flag** from the tab title while retaining it for the watermark logic.
18
+
19
+ **Final Format:** `[LastName] [FirstName] [AssignmentName]`
20
+
21
+ ## Usage & Formatting Rules
22
+
23
+ ### Enabling Draft Mode
24
+ To enable the "DRAFT" watermark across all pages, include the string `| draft` (case-insensitive) in the document header.
25
+ * **Requirement**: The `| draft` flag **must be placed after** the assignment name.
26
+ * *Example:* `English Essay | draft`
27
+
28
+ ### Works Cited
29
+ Any content following an element containing the text "Works Cited" will be moved to its own page and sorted alphabetically based on the first word of each entry.
30
+
31
+ ## Installation
32
+ Include the minified script in your project. The library will automatically detect if jQuery is present; if not, it will load it along with other necessary assets from a CDN.
33
+
34
+ ```html
35
+ <!-- Include the library at the end of your body tag -->
36
+ <script src="https://cdn.jsdelivr.net"></script>
37
+
38
+ ## Printing
39
+ Printing is **seamless**. Because the library handles all pagination, margins, and font styling via a specialized `@media print` stylesheet, you do not need to adjust your browser's print settings. Simply use `Ctrl + P` (or `Cmd + P`) and the document will print exactly as it appears on the screen.
40
+
41
+ > [!TIP]
42
+ > **Print Settings:** For the cleanest output, ensure **"Headers and Footers"** is **unchecked** in your browser's print dialog to avoid redundant browser-generated page numbers or URLs. To see the "DRAFT" watermark, ensure **"Background Graphics"** is **checked**.
43
+
44
+ ## Contributing
45
+ Contributions are welcome! If Google Docs updates its HTML export structure and changes class names (like `.c1` or `.c7`), please feel free to:
46
+ 1. **Fork** the repository.
47
+ 2. **Update** the selectors in the unminified `cleanMLAGDoc.js`.
48
+ 3. Submit a **Pull Request** with a description of the changes.
49
+
50
+ ## Dependencies
51
+ The library dynamically loads the following assets via [jsDelivr](https://www.jsdelivr.com) if they are not already present in the environment:
52
+ * [jQuery 3.7.1](https://cdn.jsdelivr.net/npm/jquery@3.7.1/)
53
+ * [Favicon.js 1.0.0](https://cdn.jsdelivr.net/npm/favicon.js@1.0.0/)
54
+
55
+ ## License
56
+ This project is licensed under the **GNU General Public License v3.0**.
57
+ Full license text available at: [https://www.gnu.org](https://www.gnu.org)
58
+
59
+ ---
60
+ **Copyright (c) 2026 Musicalisk <Musicalisk.travail@tuta.io>**
@@ -0,0 +1,246 @@
1
+ /*
2
+ * cleanMLAGDoc.js v1.0.2
3
+ * Copyright (c) 2026 Musicalisk <Musicalisk.travail@tuta.io>
4
+ * Licensed under the GNU General Public License v3.0
5
+ * Full license text: https://www.gnu.org
6
+ */
7
+
8
+ (function() {
9
+ // Configuration for external dependencies and assets
10
+ const CONFIG = {
11
+ jqueryUrl: "https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js",
12
+ faviconUrl: "https://cdn.jsdelivr.net/npm/favicon.js@1.0.0/dist/favicon.min.js",
13
+ iconFrames: [
14
+ 'https://img.icons8.com/?size=16&id=1395&format=png&color=FA5252',
15
+ 'https://img.icons8.com/?size=16&id=1395&format=png&color=20C997',
16
+ 'https://img.icons8.com/?size=16&id=1395&format=png&color=339AF0'
17
+ ]
18
+ };
19
+
20
+ // Helper to load external scripts dynamically
21
+ const loadScript = (url, callback) => {
22
+ let script = document.createElement("script");
23
+ script.src = url;
24
+ script.onload = callback;
25
+ document.head.appendChild(script);
26
+ };
27
+
28
+ // Main formatting logic
29
+ const initializeCleanMLAG = () => {
30
+ /**
31
+ * CSS INJECTION
32
+ * Defines the physical 8.5x11 inch page structure and the "DRAFT" watermark.
33
+ */
34
+ $('<style>').html(`
35
+ @page { size: 8.5in 11in; margin: 0; }
36
+ html { background: #d0d0d0 !important; width: 100% !important; }
37
+ body {
38
+ visibility: hidden;
39
+ margin: 0 !important;
40
+ padding: 40px 0 !important;
41
+ display: flex !important;
42
+ flex-direction: column !important;
43
+ align-items: center !important;
44
+ width: 100% !important;
45
+ max-width: none !important;
46
+ background: transparent !important;
47
+ font-family: "Times New Roman", serif !important;
48
+ line-height: 2.0 !important;
49
+ color: #000 !important;
50
+ }
51
+ .mla-page-container {
52
+ background: #fff !important;
53
+ box-shadow: 0 0 20px rgba(0,0,0,.2) !important;
54
+ width: 8.5in !important;
55
+ height: 11in !important;
56
+ margin: 0 auto 40px auto !important;
57
+ padding: 1in !important;
58
+ box-sizing: border-box !important;
59
+ position: relative;
60
+ overflow: hidden;
61
+ flex-shrink: 0;
62
+ }
63
+ .watermark::before {
64
+ content: 'DRAFT' !important;
65
+ position: absolute !important;
66
+ top: 50% !important;
67
+ left: 50% !important;
68
+ transform: translate(-50%, -50%) rotate(-45deg) !important;
69
+ font-size: 150px !important;
70
+ color: rgba(0,0,0,0.12) !important;
71
+ z-index: 99 !important;
72
+ pointer-events: none !important;
73
+ white-space: nowrap !important;
74
+ display: block !important;
75
+ }
76
+ .no-indent { text-indent: 0 !important; }
77
+ p { text-indent: .5in !important; margin: 0 !important; }
78
+ .header-wrapper {
79
+ position: absolute;
80
+ top: .5in;
81
+ right: 1in;
82
+ text-align: right !important;
83
+ text-indent: 0 !important;
84
+ width: 6.5in;
85
+ height: .5in;
86
+ line-height: .5in !important;
87
+ z-index: 10;
88
+ }
89
+ .hanging-indent { text-indent: -.5in !important; padding-left: .5in !important; }
90
+ .centered-title { text-align: center !important; text-indent: 0 !important; }
91
+ @media print {
92
+ html { background: #fff !important; }
93
+ body { padding: 0 !important; visibility: visible !important; }
94
+ .mla-page-container { margin: 0 auto !important; box-shadow: none !important; page-break-after: always !important; }
95
+ }
96
+ `).appendTo('head');
97
+
98
+ setTimeout(() => {
99
+ /**
100
+ * DRAFT & METADATA DETECTION
101
+ * Checks the header for the "| draft" string and extracts the assignment title.
102
+ */
103
+ const headerElement = $('.c1').first();
104
+ const headerText = headerElement.text().trim();
105
+ const isDraft = /\|\s*draft/i.test(headerText);
106
+ const assignmentName = String(headerText.replace(/\|\s*draft.*/i, '').trim() || "Assignment");
107
+
108
+ // Extract Name and Page Content
109
+ let paragraphs = $('p').filter(function() {
110
+ return $(this).text().trim().length > 0 && !$(this).hasClass('c1');
111
+ });
112
+
113
+ let firstLine = paragraphs.first().text().trim();
114
+ let nameParts = firstLine.split(' ');
115
+ let firstName = String(nameParts[0] || "First");
116
+ let lastName = String(nameParts[nameParts.length - 1] || "Last");
117
+
118
+ document.title = lastName + " " + firstName + " " + assignmentName;
119
+
120
+ // Cleanup: remove raw elements and duplicate headers
121
+ $('.c1').remove();
122
+ $('*').filter(function() {
123
+ return ($(this).css('text-align') === 'right' || $(this).css('position') === 'absolute') && $(this).text().includes(lastName);
124
+ }).remove();
125
+
126
+ let elementsToProcess = $('.doc-content > *,.c7 > *,body > *').not('script,style,.mla-page-container').get();
127
+ $('body').children().not('script,style').remove();
128
+
129
+ /**
130
+ * PAGE FACTORY
131
+ * Creates a new page and applies the "DRAFT" watermark if flagged.
132
+ */
133
+ const createPage = (pageNum) => {
134
+ let page = $('<div class="mla-page-container"></div>');
135
+ if (isDraft) page.addClass('watermark');
136
+ page.append(`<div class="header-wrapper">${lastName} ${pageNum}</div>`);
137
+ return page;
138
+ };
139
+
140
+ // Pagination state
141
+ let currentPageNum = 1;
142
+ let currentPage = createPage(currentPageNum);
143
+ let currentHeight = 0;
144
+ const maxHeight = 860; // Max vertical content height before page break
145
+ let worksCitedStarted = false;
146
+ let processedHeader = false;
147
+ let processedTitle = false;
148
+ let worksCitedItems = [];
149
+
150
+ $('body').append(currentPage);
151
+
152
+ /**
153
+ * DATE LOGIC
154
+ * Converts "MM/DD/YYYY" or "DD/MM/YYYY" styles to "DD Mon. YYYY".
155
+ */
156
+ const getMonthName = (n) => ["", "Jan.", "Feb.", "Mar.", "Apr.", "May", "June", "July", "Aug.", "Sept.", "Oct.", "Nov.", "Dec."][parseInt(n)] || "";
157
+ const dateRegex = /^(\d{1,2})[\/\-\.](\d{1,2})[\/\-\.](\d{2,4})$/;
158
+
159
+ /**
160
+ * RENDERING ENGINE
161
+ * Measures element height in a hidden container to determine if it fits on the current page.
162
+ */
163
+ const renderElement = (element) => {
164
+ let tempWrapper = $('<div style="width:6.5in;position:absolute;visibility:hidden;line-height:2.0"></div>')
165
+ .append(element.clone())
166
+ .appendTo('body');
167
+ let elementHeight = tempWrapper.outerHeight(true);
168
+ tempWrapper.remove();
169
+
170
+ if (currentHeight + elementHeight > maxHeight) {
171
+ currentPageNum++;
172
+ currentPage = createPage(currentPageNum);
173
+ $('body').append(currentPage);
174
+ currentHeight = 0;
175
+ }
176
+ currentPage.append(element);
177
+ currentHeight += elementHeight;
178
+ };
179
+
180
+ // Content processing loop
181
+ $(elementsToProcess).each(function() {
182
+ let $el = $(this);
183
+ let text = $el.text().trim();
184
+
185
+ if (text.length === 0 && !$el.is('img,table')) return;
186
+
187
+ // Detect start of Works Cited
188
+ if (text.toLowerCase().includes("works cited") && !worksCitedStarted) {
189
+ worksCitedStarted = true;
190
+ currentPageNum++;
191
+ currentPage = createPage(currentPageNum);
192
+ $('body').append(currentPage);
193
+ currentHeight = 0;
194
+ $el.addClass('centered-title no-indent');
195
+ renderElement($el);
196
+ return;
197
+ }
198
+
199
+ if (!worksCitedStarted) {
200
+ // Logic for first-page MLA header
201
+ if (!processedHeader) {
202
+ $el.addClass('no-indent');
203
+ let match = text.match(dateRegex);
204
+ if (match) {
205
+ // match[1] = Day, match[2] = Month, match[3] = Year
206
+ $el.text(match[1] + " " + getMonthName(match[2]) + " " + match[3]);
207
+ processedHeader = true;
208
+ }
209
+ } else if (!processedTitle) {
210
+ $el.addClass('centered-title no-indent');
211
+ processedTitle = true;
212
+ } else {
213
+ let html = $el.html();
214
+ if (html.includes('&nbsp;')) $el.html(html.replace(/&nbsp;/g, ' '));
215
+ }
216
+ renderElement($el);
217
+ } else {
218
+ // Gather entries for alphabetical sorting
219
+ worksCitedItems.push($el.addClass('hanging-indent no-indent'));
220
+ }
221
+ });
222
+
223
+ /**
224
+ * WORKS CITED ALPHABETICAL SORT
225
+ * Sorts entries and renders them to the final pages.
226
+ */
227
+ if (worksCitedItems.length > 0) {
228
+ worksCitedItems.sort((a, b) => a.text().trim().localeCompare(b.text().trim()))
229
+ .forEach(item => renderElement(item));
230
+ }
231
+
232
+ // Reveal document
233
+ $('body').css('visibility', 'visible');
234
+
235
+ // Optional Favicon Animation
236
+ loadScript(CONFIG.faviconUrl, () => {
237
+ if (window.favicon || window.favico) {
238
+ (window.favicon || window.favico).animate(CONFIG.iconFrames, 1750);
239
+ }
240
+ });
241
+ }, 600);
242
+ };
243
+
244
+ // Entry point: ensure jQuery is loaded
245
+ window.jQuery ? initializeCleanMLAG() : loadScript(CONFIG.jqueryUrl, initializeCleanMLAG);
246
+ })();
@@ -0,0 +1 @@
1
+ /* cleanMLAGDoc.min.js v1.0.2 | Copyright (c) 2026 Musicalisk <Musicalisk.travail@tuta.io> | Licensed under the GNU General Public License v3.0 | Full license text: https://www.gnu.org */(function(){const L={j:"https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js",f:"https://cdn.jsdelivr.net/npm/favicon.js@1.0.0/dist/favicon.min.js",i:['https://img.icons8.com/?size=16&id=1395&format=png&color=FA5252','https://img.icons8.com/?size=16&id=1395&format=png&color=20C997','https://img.icons8.com/?size=16&id=1395&format=png&color=339AF0']};const l=(u,c)=>{let s=document.createElement("script");s.src=u;s.onload=c;document.head.appendChild(s)};const r=()=>{$('<style>').html(`@page{size:8.5in 11in;margin:0}html{background:#d0d0d0!important;width:100%!important}body{visibility:hidden;margin:0!important;padding:40px 0!important;display:flex!important;flex-direction:column!important;align-items:center!important;width:100%!important;max-width:none!important;background:transparent!important;font-family:"Times New Roman",serif!important;line-height:2.0!important;color:#000!important}.mla-page-container{background:#fff!important;box-shadow:0 0 20px rgba(0,0,0,.2)!important;width:8.5in!important;height:11in!important;margin:0 auto 40px auto!important;padding:1in!important;box-sizing:border-box!important;position:relative;overflow:hidden;flex-shrink:0}.watermark::before{content:'DRAFT'!important;position:absolute!important;top:50%!important;left:50%!important;transform:translate(-50%,-50%) rotate(-45deg)!important;font-size:150px!important;color:rgba(0,0,0,0.12)!important;z-index:99!important;pointer-events:none!important;white-space:nowrap!important;display:block!important}.no-indent{text-indent:0!important}p{text-indent:.5in!important;margin:0!important}.header-wrapper{position:absolute;top:.5in;right:1in;text-align:right!important;text-indent:0!important;width:6.5in;height:.5in;line-height:.5in!important;z-index:10}.hanging-indent{text-indent:-.5in!important;padding-left:.5in!important}.centered-title{text-align:center!important;text-indent:0!important}@media print{html{background:#fff!important}body{padding:0!important;visibility:visible!important}.mla-page-container{margin:0 auto!important;box-shadow:none!important;page-break-after:always!important}}`).appendTo('head');setTimeout(()=>{const h=$('.c1').first(),ht=h.text().trim();const isD=/\|\s*draft/i.test(ht);const an=String(ht.replace(/\|\s*draft.*/i,'').trim()||"Assignment");let pgs=$('p').filter(function(){return $(this).text().trim().length>0&&!$(this).hasClass('c1')});let s=pgs.first().text().trim(),n=s.split(' '),f=String(n[0]||"First"),ln=String(n[n.length-1]||"Last");document.title=ln+" "+f+" "+an;$('.c1').remove();$('*').filter(function(){return($(this).css('text-align')==='right'||$(this).css('position')==='absolute')&&$(this).text().includes(ln)}).remove();let e=$('.doc-content > *,.c7 > *,body > *').not('script,style,.mla-page-container').get();$('body').children().not('script,style').remove();const p=(v)=>{let g=$('<div class="mla-page-container"></div>');if(isD)g.addClass('watermark');g.append(`<div class="header-wrapper">${ln} ${v}</div>`);return g};let c=1,u=p(c),y=0,x=860,z=false,ph=false,pt=false,ci=[];$('body').append(u);const gm=(n)=>["","Jan.","Feb.","Mar.","Apr.","May","June","July","Aug.","Sept.","Oct.","Nov.","Dec."][parseInt(n)]||"",dr=/^(\d{1,2})[\/\-\.](\d{1,2})[\/\-\.](\d{2,4})$/,re=(o)=>{let k=$('<div style="width:6.5in;position:absolute;visibility:hidden;line-height:2.0"></div>').append(o.clone()).appendTo('body'),q=k.outerHeight(true);k.remove();if(y+q>x){c++;u=p(c);$('body').append(u);y=0}u.append(o);y+=q};$(e).each(function(j){let o=$(this),v=o.text().trim();if(v.length===0&&!o.is('img,table'))return;if(v.toLowerCase().includes("works cited")&&!z){z=true;c++;u=p(c);$('body').append(u);y=0;o.addClass('centered-title no-indent');re(o);return}if(!z){if(!ph){let m=v.match(dr);if(m){o.text(m[1]+" "+gm(m[2])+" "+m[3]);ph=true}o.addClass('no-indent')}else if(!pt){o.addClass('centered-title no-indent');pt=true}else{let h=o.html();if(h.includes('&nbsp;'))o.html(h.replace(/&nbsp;/g,' '))}re(o)}else{ci.push(o.addClass('hanging-indent no-indent'))}});if(ci.length>0){ci.sort((a,b)=>a.text().trim().localeCompare(b.text().trim())).forEach(i=>re(i))} $('body').css('visibility','visible');l(L.f,()=>{if(window.favicon||window.favico)(window.favicon||window.favico).animate(L.i,1750)})},600)};window.jQuery?r():l(L.j,r)})();
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "clean-mla-gdoc",
3
+ "version": "1.0.2",
4
+ "description": "A lightweight JavaScript library to reformat Google Docs HTML exports into MLA Style layout with auto-pagination and Works Cited sorting.",
5
+ "main": "dist/cleanMLAGDoc.js",
6
+ "browser": "dist/cleanMLAGDoc.min.js",
7
+ "keywords": [
8
+ "\"mla\"",
9
+ "\"google-docs\"",
10
+ "\"academic\""
11
+ ],
12
+ "homepage": "https://github.com/Musicalisk/clean-mla-gdoc#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/Musicalisk/clean-mla-gdoc/issues"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/Musicalisk/clean-mla-gdoc.git"
19
+ },
20
+ "license": "ISC",
21
+ "author": "\"Musicalisk <Musicalisk.travail@tuta.io>",
22
+ "type": "commonjs",
23
+ "scripts": {
24
+ "test": "echo \"Error: no test specified\" && exit 1"
25
+ }
26
+ }