From 2a3c7ab7c81df74ccd5ab27e301cb3aec1ac08c1 Mon Sep 17 00:00:00 2001 From: Tom Rodriguez Date: Wed, 10 Aug 2022 17:04:09 -0700 Subject: [PATCH] Initial commit --- .gitignore | 47 +++ LICENSE | 360 +++++++++++++++++++++++ README.md | 13 + lang/en.json | 27 ++ module.json | 43 +++ scripts/split-html.js | 150 ++++++++++ scripts/split-join-journal.js | 286 ++++++++++++++++++ templates/split-journal-dialog.html | 9 + templates/split-journal-page-dialog.html | 19 ++ 9 files changed, 954 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 lang/en.json create mode 100644 module.json create mode 100644 scripts/split-html.js create mode 100644 scripts/split-join-journal.js create mode 100644 templates/split-journal-dialog.html create mode 100644 templates/split-journal-page-dialog.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba4e784 --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +# Dependency directories +node_modules/ +jspm_packages/ + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Visual Studio +.vscode/ +*.code-workspace + +# macOS specific files +# general +.DS_Store +.AppleDouble +.LSOverride + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Build files +build/ + +# Distrubution files +dist/ + +# Package builds +package/ + +# Hidden files +nogit/ + +# Foundry Project Creator config file +foundryconfig.json diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a50eacf --- /dev/null +++ b/LICENSE @@ -0,0 +1,360 @@ +Creative Commons Legal Code + +Attribution-NonCommercial-ShareAlike 3.0 Unported + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR + DAMAGES RESULTING FROM ITS USE. + +License + +THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE +COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY +COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS +AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. + +BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE +TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY +BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS +CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND +CONDITIONS. + +1. Definitions + + a. "Adaptation" means a work based upon the Work, or upon the Work and + other pre-existing works, such as a translation, adaptation, + derivative work, arrangement of music or other alterations of a + literary or artistic work, or phonogram or performance and includes + cinematographic adaptations or any other form in which the Work may be + recast, transformed, or adapted including in any form recognizably + derived from the original, except that a work that constitutes a + Collection will not be considered an Adaptation for the purpose of + this License. For the avoidance of doubt, where the Work is a musical + work, performance or phonogram, the synchronization of the Work in + timed-relation with a moving image ("synching") will be considered an + Adaptation for the purpose of this License. + b. "Collection" means a collection of literary or artistic works, such as + encyclopedias and anthologies, or performances, phonograms or + broadcasts, or other works or subject matter other than works listed + in Section 1(g) below, which, by reason of the selection and + arrangement of their contents, constitute intellectual creations, in + which the Work is included in its entirety in unmodified form along + with one or more other contributions, each constituting separate and + independent works in themselves, which together are assembled into a + collective whole. A work that constitutes a Collection will not be + considered an Adaptation (as defined above) for the purposes of this + License. + c. "Distribute" means to make available to the public the original and + copies of the Work or Adaptation, as appropriate, through sale or + other transfer of ownership. + d. "License Elements" means the following high-level license attributes + as selected by Licensor and indicated in the title of this License: + Attribution, Noncommercial, ShareAlike. + e. "Licensor" means the individual, individuals, entity or entities that + offer(s) the Work under the terms of this License. + f. "Original Author" means, in the case of a literary or artistic work, + the individual, individuals, entity or entities who created the Work + or if no individual or entity can be identified, the publisher; and in + addition (i) in the case of a performance the actors, singers, + musicians, dancers, and other persons who act, sing, deliver, declaim, + play in, interpret or otherwise perform literary or artistic works or + expressions of folklore; (ii) in the case of a phonogram the producer + being the person or legal entity who first fixes the sounds of a + performance or other sounds; and, (iii) in the case of broadcasts, the + organization that transmits the broadcast. + g. "Work" means the literary and/or artistic work offered under the terms + of this License including without limitation any production in the + literary, scientific and artistic domain, whatever may be the mode or + form of its expression including digital form, such as a book, + pamphlet and other writing; a lecture, address, sermon or other work + of the same nature; a dramatic or dramatico-musical work; a + choreographic work or entertainment in dumb show; a musical + composition with or without words; a cinematographic work to which are + assimilated works expressed by a process analogous to cinematography; + a work of drawing, painting, architecture, sculpture, engraving or + lithography; a photographic work to which are assimilated works + expressed by a process analogous to photography; a work of applied + art; an illustration, map, plan, sketch or three-dimensional work + relative to geography, topography, architecture or science; a + performance; a broadcast; a phonogram; a compilation of data to the + extent it is protected as a copyrightable work; or a work performed by + a variety or circus performer to the extent it is not otherwise + considered a literary or artistic work. + h. "You" means an individual or entity exercising rights under this + License who has not previously violated the terms of this License with + respect to the Work, or who has received express permission from the + Licensor to exercise rights under this License despite a previous + violation. + i. "Publicly Perform" means to perform public recitations of the Work and + to communicate to the public those public recitations, by any means or + process, including by wire or wireless means or public digital + performances; to make available to the public Works in such a way that + members of the public may access these Works from a place and at a + place individually chosen by them; to perform the Work to the public + by any means or process and the communication to the public of the + performances of the Work, including by public digital performance; to + broadcast and rebroadcast the Work by any means including signs, + sounds or images. + j. "Reproduce" means to make copies of the Work by any means including + without limitation by sound or visual recordings and the right of + fixation and reproducing fixations of the Work, including storage of a + protected performance or phonogram in digital form or other electronic + medium. + +2. Fair Dealing Rights. Nothing in this License is intended to reduce, +limit, or restrict any uses free from copyright or rights arising from +limitations or exceptions that are provided for in connection with the +copyright protection under copyright law or other applicable laws. + +3. License Grant. Subject to the terms and conditions of this License, +Licensor hereby grants You a worldwide, royalty-free, non-exclusive, +perpetual (for the duration of the applicable copyright) license to +exercise the rights in the Work as stated below: + + a. to Reproduce the Work, to incorporate the Work into one or more + Collections, and to Reproduce the Work as incorporated in the + Collections; + b. to create and Reproduce Adaptations provided that any such Adaptation, + including any translation in any medium, takes reasonable steps to + clearly label, demarcate or otherwise identify that changes were made + to the original Work. For example, a translation could be marked "The + original work was translated from English to Spanish," or a + modification could indicate "The original work has been modified."; + c. to Distribute and Publicly Perform the Work including as incorporated + in Collections; and, + d. to Distribute and Publicly Perform Adaptations. + +The above rights may be exercised in all media and formats whether now +known or hereafter devised. The above rights include the right to make +such modifications as are technically necessary to exercise the rights in +other media and formats. Subject to Section 8(f), all rights not expressly +granted by Licensor are hereby reserved, including but not limited to the +rights described in Section 4(e). + +4. Restrictions. The license granted in Section 3 above is expressly made +subject to and limited by the following restrictions: + + a. You may Distribute or Publicly Perform the Work only under the terms + of this License. You must include a copy of, or the Uniform Resource + Identifier (URI) for, this License with every copy of the Work You + Distribute or Publicly Perform. You may not offer or impose any terms + on the Work that restrict the terms of this License or the ability of + the recipient of the Work to exercise the rights granted to that + recipient under the terms of the License. You may not sublicense the + Work. You must keep intact all notices that refer to this License and + to the disclaimer of warranties with every copy of the Work You + Distribute or Publicly Perform. When You Distribute or Publicly + Perform the Work, You may not impose any effective technological + measures on the Work that restrict the ability of a recipient of the + Work from You to exercise the rights granted to that recipient under + the terms of the License. This Section 4(a) applies to the Work as + incorporated in a Collection, but this does not require the Collection + apart from the Work itself to be made subject to the terms of this + License. If You create a Collection, upon notice from any Licensor You + must, to the extent practicable, remove from the Collection any credit + as required by Section 4(d), as requested. If You create an + Adaptation, upon notice from any Licensor You must, to the extent + practicable, remove from the Adaptation any credit as required by + Section 4(d), as requested. + b. You may Distribute or Publicly Perform an Adaptation only under: (i) + the terms of this License; (ii) a later version of this License with + the same License Elements as this License; (iii) a Creative Commons + jurisdiction license (either this or a later license version) that + contains the same License Elements as this License (e.g., + Attribution-NonCommercial-ShareAlike 3.0 US) ("Applicable License"). + You must include a copy of, or the URI, for Applicable License with + every copy of each Adaptation You Distribute or Publicly Perform. You + may not offer or impose any terms on the Adaptation that restrict the + terms of the Applicable License or the ability of the recipient of the + Adaptation to exercise the rights granted to that recipient under the + terms of the Applicable License. You must keep intact all notices that + refer to the Applicable License and to the disclaimer of warranties + with every copy of the Work as included in the Adaptation You + Distribute or Publicly Perform. When You Distribute or Publicly + Perform the Adaptation, You may not impose any effective technological + measures on the Adaptation that restrict the ability of a recipient of + the Adaptation from You to exercise the rights granted to that + recipient under the terms of the Applicable License. This Section 4(b) + applies to the Adaptation as incorporated in a Collection, but this + does not require the Collection apart from the Adaptation itself to be + made subject to the terms of the Applicable License. + c. You may not exercise any of the rights granted to You in Section 3 + above in any manner that is primarily intended for or directed toward + commercial advantage or private monetary compensation. The exchange of + the Work for other copyrighted works by means of digital file-sharing + or otherwise shall not be considered to be intended for or directed + toward commercial advantage or private monetary compensation, provided + there is no payment of any monetary compensation in con-nection with + the exchange of copyrighted works. + d. If You Distribute, or Publicly Perform the Work or any Adaptations or + Collections, You must, unless a request has been made pursuant to + Section 4(a), keep intact all copyright notices for the Work and + provide, reasonable to the medium or means You are utilizing: (i) the + name of the Original Author (or pseudonym, if applicable) if supplied, + and/or if the Original Author and/or Licensor designate another party + or parties (e.g., a sponsor institute, publishing entity, journal) for + attribution ("Attribution Parties") in Licensor's copyright notice, + terms of service or by other reasonable means, the name of such party + or parties; (ii) the title of the Work if supplied; (iii) to the + extent reasonably practicable, the URI, if any, that Licensor + specifies to be associated with the Work, unless such URI does not + refer to the copyright notice or licensing information for the Work; + and, (iv) consistent with Section 3(b), in the case of an Adaptation, + a credit identifying the use of the Work in the Adaptation (e.g., + "French translation of the Work by Original Author," or "Screenplay + based on original Work by Original Author"). The credit required by + this Section 4(d) may be implemented in any reasonable manner; + provided, however, that in the case of a Adaptation or Collection, at + a minimum such credit will appear, if a credit for all contributing + authors of the Adaptation or Collection appears, then as part of these + credits and in a manner at least as prominent as the credits for the + other contributing authors. For the avoidance of doubt, You may only + use the credit required by this Section for the purpose of attribution + in the manner set out above and, by exercising Your rights under this + License, You may not implicitly or explicitly assert or imply any + connection with, sponsorship or endorsement by the Original Author, + Licensor and/or Attribution Parties, as appropriate, of You or Your + use of the Work, without the separate, express prior written + permission of the Original Author, Licensor and/or Attribution + Parties. + e. For the avoidance of doubt: + + i. Non-waivable Compulsory License Schemes. In those jurisdictions in + which the right to collect royalties through any statutory or + compulsory licensing scheme cannot be waived, the Licensor + reserves the exclusive right to collect such royalties for any + exercise by You of the rights granted under this License; + ii. Waivable Compulsory License Schemes. In those jurisdictions in + which the right to collect royalties through any statutory or + compulsory licensing scheme can be waived, the Licensor reserves + the exclusive right to collect such royalties for any exercise by + You of the rights granted under this License if Your exercise of + such rights is for a purpose or use which is otherwise than + noncommercial as permitted under Section 4(c) and otherwise waives + the right to collect royalties through any statutory or compulsory + licensing scheme; and, + iii. Voluntary License Schemes. The Licensor reserves the right to + collect royalties, whether individually or, in the event that the + Licensor is a member of a collecting society that administers + voluntary licensing schemes, via that society, from any exercise + by You of the rights granted under this License that is for a + purpose or use which is otherwise than noncommercial as permitted + under Section 4(c). + f. Except as otherwise agreed in writing by the Licensor or as may be + otherwise permitted by applicable law, if You Reproduce, Distribute or + Publicly Perform the Work either by itself or as part of any + Adaptations or Collections, You must not distort, mutilate, modify or + take other derogatory action in relation to the Work which would be + prejudicial to the Original Author's honor or reputation. Licensor + agrees that in those jurisdictions (e.g. Japan), in which any exercise + of the right granted in Section 3(b) of this License (the right to + make Adaptations) would be deemed to be a distortion, mutilation, + modification or other derogatory action prejudicial to the Original + Author's honor and reputation, the Licensor will waive or not assert, + as appropriate, this Section, to the fullest extent permitted by the + applicable national law, to enable You to reasonably exercise Your + right under Section 3(b) of this License (right to make Adaptations) + but not otherwise. + +5. Representations, Warranties and Disclaimer + +UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING AND TO THE +FULLEST EXTENT PERMITTED BY APPLICABLE LAW, LICENSOR OFFERS THE WORK AS-IS +AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE +WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT +LIMITATION, WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, +ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT +DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED +WARRANTIES, SO THIS EXCLUSION MAY NOT APPLY TO YOU. + +6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE +LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR +ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES +ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS +BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. Termination + + a. This License and the rights granted hereunder will terminate + automatically upon any breach by You of the terms of this License. + Individuals or entities who have received Adaptations or Collections + from You under this License, however, will not have their licenses + terminated provided such individuals or entities remain in full + compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will + survive any termination of this License. + b. Subject to the above terms and conditions, the license granted here is + perpetual (for the duration of the applicable copyright in the Work). + Notwithstanding the above, Licensor reserves the right to release the + Work under different license terms or to stop distributing the Work at + any time; provided, however that any such election will not serve to + withdraw this License (or any other license that has been, or is + required to be, granted under the terms of this License), and this + License will continue in full force and effect unless terminated as + stated above. + +8. Miscellaneous + + a. Each time You Distribute or Publicly Perform the Work or a Collection, + the Licensor offers to the recipient a license to the Work on the same + terms and conditions as the license granted to You under this License. + b. Each time You Distribute or Publicly Perform an Adaptation, Licensor + offers to the recipient a license to the original Work on the same + terms and conditions as the license granted to You under this License. + c. If any provision of this License is invalid or unenforceable under + applicable law, it shall not affect the validity or enforceability of + the remainder of the terms of this License, and without further action + by the parties to this agreement, such provision shall be reformed to + the minimum extent necessary to make such provision valid and + enforceable. + d. No term or provision of this License shall be deemed waived and no + breach consented to unless such waiver or consent shall be in writing + and signed by the party to be charged with such waiver or consent. + e. This License constitutes the entire agreement between the parties with + respect to the Work licensed here. There are no understandings, + agreements or representations with respect to the Work not specified + here. Licensor shall not be bound by any additional provisions that + may appear in any communication from You. This License may not be + modified without the mutual written agreement of the Licensor and You. + f. The rights granted under, and the subject matter referenced, in this + License were drafted utilizing the terminology of the Berne Convention + for the Protection of Literary and Artistic Works (as amended on + September 28, 1979), the Rome Convention of 1961, the WIPO Copyright + Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 + and the Universal Copyright Convention (as revised on July 24, 1971). + These rights and subject matter take effect in the relevant + jurisdiction in which the License terms are sought to be enforced + according to the corresponding provisions of the implementation of + those treaty provisions in the applicable national law. If the + standard suite of rights granted under applicable copyright law + includes additional rights not granted under this License, such + additional rights are deemed to be included in the License; this + License is not intended to restrict the license of any rights under + applicable law. + + +Creative Commons Notice + + Creative Commons is not a party to this License, and makes no warranty + whatsoever in connection with the Work. Creative Commons will not be + liable to You or any party on any legal theory for any damages + whatsoever, including without limitation any general, special, + incidental or consequential damages arising in connection to this + license. Notwithstanding the foregoing two (2) sentences, if Creative + Commons has expressly identified itself as the Licensor hereunder, it + shall have all rights and obligations of Licensor. + + Except for the limited purpose of indicating to the public that the + Work is licensed under the CCPL, Creative Commons does not authorize + the use by either party of the trademark "Creative Commons" or any + related trademark or logo of Creative Commons without the prior + written consent of Creative Commons. Any permitted use will be in + compliance with Creative Commons' then-current trademark usage + guidelines, as may be published on its website or otherwise made + available upon request from time to time. For the avoidance of doubt, + this trademark restriction does not form part of this License. + + Creative Commons may be contacted at https://creativecommons.org/. diff --git a/README.md b/README.md new file mode 100644 index 0000000..cd73989 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# Split Join Journal +[![Version (latest)](https://img.shields.io/github/v/release/toastygm/split-join-journal)](https://github.com/toastygm/split-join-journal/releases/latest) +[![Forge Installs](https://img.shields.io/badge/dynamic/json?label=Forge%20Installs&query=package.installs&suffix=%25&url=https%3A%2F%2Fforge-vtt.com%2Fapi%2Fbazaar%2Fpackage%2Fsplit-join-journal&colorB=4aa94a)](https://forge-vtt.com/bazaar#package=split-join-journal) +[![GitHub downloads (latest)](https://img.shields.io/badge/dynamic/json?label=Downloads@latest&query=assets[?(@.name.includes('zip'))].download_count&url=https://api.github.com/repos/toastygm/split-join-journal/releases/latest&color=green)](https://github.com/toastygm/split-join-journal/releases/latest) + + +Foundry VTT v10 introduced the new Journals V2. This module lets you manage pages and Journals, merging and organizing them simply. + +With this module you can: +1. Split a single Journal Page based on HTML headings into separate pages, either in the same Journal or in a separate Journal. +2. Split all of the pages in a Journal into separate Journals (one page per journal), either in a new folder or in the existing folder. +3. Join all of the Journals (and all of their pages) within a single folder into a new single Journal, with all of the pages from all of the Journals. + diff --git a/lang/en.json b/lang/en.json new file mode 100644 index 0000000..ed37ec3 --- /dev/null +++ b/lang/en.json @@ -0,0 +1,27 @@ +{ + "SPLITJOINJNL.SPLITPAGE.title": "Split Journal Page", + "SPLITJOINJNL.SPLITPAGE.splitSubmit": "Split Journal Page", + "SPLITJOINJNL.SPLITPAGE.contextMenu": "Split Journal Page", + "SPLITJOINJNL.SPLITPAGE.description": "Split the journal page '{name}' into multiple pages.", + "SPLITJOINJNL.SPLITPAGE.splitOn": "Split on", + "SPLITJOINJNL.SPLITPAGE.journalNameDesc": "Provide a new journal name, or blank to split into existing journal.", + "SPLITJOINJNL.SPLITPAGE.journalName": "Journal Name", + "SPLITJOINJNL.SPLITPAGE.errorNoHeadings": "Journal {journalName} page {pageName} does not have any headings, cannot split.", + "SPLITJOINJNL.SPLITPAGE.errorNotText": "Journal {journalName} page {pageName} does not have text, cannot split.", + "SPLITJOINJNL.SPLITPAGE.errorJournalPageEmpty": "This Journal page is empty. There is nothing to split.", + "SPLITJOINJNL.SPLITJOURNAL.title": "Split Journal", + "SPLITJOINJNL.SPLITJOURNAL.splitSubmit": "Split Journal", + "SPLITJOINJNL.SPLITJOURNAL.contextMenu": "Split Journal", + "SPLITJOINJNL.SPLITJOURNAL.description": "Split Journal", + "SPLITJOINJNL.SPLITJOURNAL.folderNameDesc": "Provide a new folder name, or blank to split into existing folder.", + "SPLITJOINJNL.SPLITJOURNAL.folderName": "Folder Name", + "SPLITJOINJNL.MERGEFOLDER.title": "Join Journal Pages", + "SPLITJOINJNL.MERGEFOLDER.contextMenu": "Join Journal Pages", + "SPLITJOINJNL.HEADING1": "Heading 1", + "SPLITJOINJNL.HEADING2": "Heading 2", + "SPLITJOINJNL.HEADING3": "Heading 3", + "SPLITJOINJNL.HEADING4": "Heading 4", + "SPLITJOINJNL.HEADING5": "Heading 5", + "SPLITJOINJNL.HEADING6": "Heading 6", + "SPLITJOINJNL.HEADING7": "Heading 7" +} diff --git a/module.json b/module.json new file mode 100644 index 0000000..7470800 --- /dev/null +++ b/module.json @@ -0,0 +1,43 @@ +{ + "id": "split-join-journal", + "title": "Split-Join Journal", + "description": "Split and Join pages within a Journal or Journals within a folder.", + "version": "1.0.0", + "url": "https://github.com/toastygm/split-join-journal", + "bugs": "https://github.com/toastygm/split-join-journal/issues", + "license": "LICENSE", + "readme": "README.md", + "authors": [ + { + "name": "Toasty", + "discord": "toasty#8538", + "flags": {} + } + ], + "flags": { + "allowBugReporter": true + }, + "compatibility": { + "minimum": "10", + "verified": "10" + }, + "scripts": [ + "scripts/split-html.js", + "scripts/split-join-journal.js" + ], + "esmodules": [], + "styles": [], + "languages": [ + { + "lang": "en", + "name": "English", + "path": "lang/en.json" + } + ], + "packs": [], + "system": [], + "dependencies": [], + "socket": true, + "manifest": "https://github.com/toastygm/split-join-journal/releases/latest/download/module.json", + "download": "https://github.com/toastygm/split-join-journal/releases/download/v1.1.2/module.zip" +} diff --git a/scripts/split-html.js b/scripts/split-html.js new file mode 100644 index 0000000..25d0968 --- /dev/null +++ b/scripts/split-html.js @@ -0,0 +1,150 @@ +// This code comes from +// https://github.com/apostrophecms/split-html +// Copyright (c) 2016 P'unk Avenue LLC + +(function() { + var cheerio; + if (typeof window === 'undefined') { + // node + module.exports = splitHtml; + cheerio = require('cheerio'); + } else { + window.splitHtml = splitHtml; + // In the browser, use actual jQuery in place of Cheerio. + // Create a simulated cheerio object. + cheerio = { + load: function(html) { + var $wrapper = jQuery('
'); + var $el = jQuery(html); + $wrapper.append($el); + function c(s) { + if (s[0] === '<') { + return jQuery(s); + } + return $wrapper.find(s); + } + c.html = function() { + return $wrapper.html(); + }; + return c; + }, + }; + } + function splitHtml(html, splitOn, test) { + if (!test) { + test = function($el) { + return true; + }; + } + var result = []; + var splitAttr = 'data-' + token(); + var ignoreAttr = 'data-' + token(); + var $; + var $matches; + var i; + var $match; + var $wrapper; + var tag; + var second; + while (true) { + $ = cheerio.load(html); + $matches = $(splitOn); + $match = null; + for (i = 0; (i < $matches.length); i++) { + $match = $matches.eq(i); + if ((!$match.attr(ignoreAttr)) && test($match)) { + break; + } else { + $match.attr(ignoreAttr, '1'); + } + $match = null; + } + if (!$match) { + result.push(html); + break; + } + $match.attr(splitAttr, '1'); + var markup = $.html(); + var splitAt = markup.indexOf(splitAttr); + var leftAt = markup.lastIndexOf('<', splitAt); + if (leftAt === -1) { + result.push(html); + break; + } + var first = markup.substr(0, leftAt); + + // For the second segment we need to reopen the + // open tags from the first segment. Reconstruct that. + + var reopen = ''; + $wrapper = cheerio.load('
')('div').eq(0); + var $parents = $match.parents(); + for (i = 0; (i < $parents.length); i++) { + var $original = $parents.eq(i); + if ($original.is('[data-cheerio-root]')) { + // Simulated cheerio used in browser has + // a wrapper element + break; + } + var $parent = $original.clone(); + $parent.empty(); + $wrapper.empty(); + $wrapper.append($parent); + var parentMarkup = $wrapper.html(); + var endTagAt = parentMarkup.indexOf('>'); + tag = tagName($parent); + // Cheerio tolerates missing closing tags, + // but real jQuery will discard any text + // preceding them, so play nice + first += ''; + reopen = parentMarkup.substr(0, endTagAt + 1) + reopen; + } + + // We can't just split off the next fragment at + // > because the matching tag may be a container. + // Move it to a wrapper to get its full markup, + // then remove it from the original document. The + // remainder of the original document now begins + // where the matching tag used to + + markup = $.html(); + + $wrapper = cheerio.load('
')('div').eq(0); + $match.removeAttr(splitAttr); + $wrapper.append($match); + tag = $wrapper.html(); + $match.remove(); + markup = $.html(); + second = reopen + markup.substr(leftAt); + // Let Cheerio close the open tags in the + // first segment for us. Also mop up the attributes + // we used to mark elements that matched the selector + // but didn't match our test function + first = cleanup(first); + result.push(first); + result.push(tag); + html = cleanup(second); + } + return result; + // Use Cheerio to strip out any attributes we used to keep + // track of our work, then generate new HTML. This also + // closes any tags we opened but did not close. + function cleanup(html) { + html = cheerio.load(html); + html('[' + ignoreAttr + ']').removeAttr(ignoreAttr); + html = html.html(); + return html; + } + + function token() { + return Math.floor(Math.random() * 1000000000).toString(); + } + } + + function tagName($el) { + // Different in DOM and Cheerio. Cheerio + // doesn't support prop() either. + return $el[0].tagName || $el[0].name; + } +})(); + diff --git a/scripts/split-join-journal.js b/scripts/split-join-journal.js new file mode 100644 index 0000000..54d24c6 --- /dev/null +++ b/scripts/split-join-journal.js @@ -0,0 +1,286 @@ +function splitContent(page, heading) { + if (!page || !heading) return []; + + const parts = splitHtml(page.text.content, heading); + const pageEntries = []; + for (let idx = 0; idx < parts.length; idx++) { + let newName; + let newContent = parts[idx]; + if (idx == 0) { + newName = "Header"; + const header = newContent.trim(); + if (header == "") continue; + } else { + newName = $(parts[idx]).text(); + newContent = parts[++idx]; + } + + const pageData = { + "name": newName || " ", + } + + if (newContent) { + pageData["text.content"] = newContent; + } + + pageEntries.push(pageData); + } + + return pageEntries; +} + +function currentHeadings(html) { + const content = $("
" + html + "
"); + const possibleHeadings = { + "h1": game.i18n.localize("SPLITJOINJNL.HEADING1"), + "h2": game.i18n.localize("SPLITJOINJNL.HEADING2"), + "h3": game.i18n.localize("SPLITJOINJNL.HEADING3"), + "h4": game.i18n.localize("SPLITJOINJNL.HEADING4"), + "h5": game.i18n.localize("SPLITJOINJNL.HEADING5"), + "h6": game.i18n.localize("SPLITJOINJNL.HEADING6"), + "h7": game.i18n.localize("SPLITJOINJNL.HEADING7") + } + + const existingHeadings = []; + + for (const heading in possibleHeadings) { + const parts = content.find(heading); + if (parts.length > 0) { + existingHeadings.push([heading, possibleHeadings[heading]]); + } + } + + return existingHeadings; +} + +function handleSplitPageContextMenu(html, options) { + + options.push({ + name: game.i18n.localize("SPLITJOINJNL.SPLITPAGE.contextMenu"), + icon: '', + condition: game.user.isGM, + callback: async (header) => { + const pageId = header.data("page-id"); + const page = game.journal.reduce((foundPage, jnl) => { + if (!foundPage) { + foundPage = jnl.pages.get(pageId); + } + return foundPage; + }, undefined); + + // only enable context item for text pages, otherwise ignore + if (page?.type != "text") { + const msg = game.i18n.format("SPLITJOINJNL.SPLITPAGE.errorNotText", {journalName: page.parent.name, pageName: page.name}); + ui.notifications.info(msg); + console.log(`split-join-journal | ${msg}`); + return; + } + + const availHeadings = currentHeadings(page.text.content); + if (availHeadings.length) { + const splitJournalPageDialog = 'modules/split-join-journal/templates/split-journal-page-dialog.html'; + const dialogOptions = { + "defaultJournalName": page.name, + "headings": {} + }; + + availHeadings.forEach(([key, val]) => { + dialogOptions.headings[key] = val; + if (!dialogOptions.defaultHeading) dialogOptions.defaultHeading = key; + }); + + const dlghtml = await renderTemplate(splitJournalPageDialog, dialogOptions); + + // request header level and new journal name + Dialog.prompt({ + title: game.i18n.localize("SPLITJOINJNL.SPLITPAGE.title"), + content: dlghtml.trim(), + label: game.i18n.localize("SPLITJOINJNL.SPLITPAGE.splitSubmit"), + rejectClose: false, + callback: async html => { + const form = html[0].querySelector("form"); + const selectedHeading = form.selectedHeading.value; + const newJournalName = form.journalName.value; + splitJournalPageIntoSeparatePages(selectedHeading, page, newJournalName); + } + }); + } else { + const msg = game.i18n.format("SPLITJOINJNL.SPLITPAGE.errorNoHeadings", {journalName: page.parent.name, pageName: page.name}); + ui.notifications.info(msg); + console.log(`split-join-journal | ${msg}`); + } + } + }); +} + +/** + * Add context menu item to a Journal to extract a JournalEntry page into + * @param {*} html + * @param {*} options + */ +async function handleJournalContextMenu(html, options) { + options.push({ + name: game.i18n.localize("SPLITJOINJNL.SPLITJOURNAL.contextMenu"), + icon: '', + condition: game.user.isGM, + callback: async (header) => { + const journalId = header.data("document-id"); + const journal = game.journal.get(journalId); + const splitJournalDialog = 'modules/split-join-journal/templates/split-journal-dialog.html'; + const dialogOptions = { + "defaultFolderName": journal.name + }; + + const dlghtml = await renderTemplate(splitJournalDialog, dialogOptions); + + // request header level and new journal name + Dialog.prompt({ + title: game.i18n.localize("SPLITJOINJNL.SPLITJOURNAL.title"), + content: dlghtml.trim(), + label: game.i18n.localize("SPLITJOINJNL.SPLITJOURNAL.splitSubmit"), + rejectClose: false, + callback: async html => { + const form = html[0].querySelector("form"); + const folderName = form.folderName.value; + splitJournalPagesIntoSeparateJournals(journal, folderName); + } + }); + + } + }); +} + +/** + * Add context menu item to a folder to merge journals in a folder + * into pages of a new journal + * @param {String} html + * @param {Object} options + */ +async function handleJournalFolderContextMenu(html, options) { + options.push({ + name: game.i18n.localize("SPLITJOINJNL.MERGEFOLDER.contextMenu"), + icon: '', + condition: li => game.user.isGM && game.folders.get(li.parent().data("folder-id"))?.contents.length, + callback: async (header) => { + const journalFolderId = header.parent().data("folder-id"); + const folder = game.folders.get(journalFolderId); + mergeJournalFolderIntoSingleJournal(folder); + } + }); +} + +/** + * Split each of the pages of a Journal into separate Journals (with one + * page each). + * + * @param {JournalEntry} journal Journal to split + * @param {String} newFolderName If non-empty, the name of the new folder where journals + * should be placed; otherwise journals will be placed in same folder as original journal + * @returns + */ +async function splitJournalPagesIntoSeparateJournals(journal, newFolderName) { + let folder = journal.folder; + + if (!journal.pages.size) { + ui.notification.info(`Journal ${journal.name} has no pages`); + return; + } + + if (newFolderName) { + folder = await Folder.create({name: newFolderName, folder: journal.folder, type: "JournalEntry", sorting: "m"}); + } + + for (let page of journal.pages) { + const newJournal = await JournalEntry.create({name: page.name, folder: folder, sort: page.sort}); + + const pageData = { + name: page.name, + type: page.type, + src: page.src + } + + switch (page.type) { + case "text": + Object.entries(flattenObject(page.text)).forEach(([key, val]) => pageData[`text.${key}`] = val ); + break; + + case "image": + Object.entries(flattenObject(page.image)).forEach(([key, val]) => pageData[`text.${key}`] = val ); + break; + + case "video": + Object.entries(flattenObject(page.video)).forEach(([key, val]) => pageData[`text.${key}`] = val ); + break; + } + + // Create a single page in the new journal with old page data + JournalEntryPage.create(pageData, {parent: newJournal}); + } + +} + +/** + * Split Journal Page into separate pages based on HTML headings. + * @param {String} targetHeading HTML heading level to break on (e.g., h1) + * @param {JournalEntryPage} page + * @param {String} newJournalName Name of new journal to create; otherwise all pages will be created + * in current journal. + */ +async function splitJournalPageIntoSeparatePages(targetHeading, page, newJournalName) { + const pageData = splitContent(page, targetHeading); + + if (!pageData?.length) { + const msg = game.i18n.localize("SPLITJOINJNL.SPLITPAGE.errorJournalPageEmpty"); + ui.notifications.info(msg); + console.log(`split-join-journal | ${msg}`); + return; + } + + let journal = page.parent; + + if (newJournalName.trim()) { + // Journal name provided, so create pages in new journal + journal = await JournalEntry.create({name: newJournalName.trim(), folder: page.parent.folder}); + } + + await JournalEntryPage.createDocuments(pageData, {parent: journal}); +} + +/** + * Merge all of the Journal pages in a single folder into a new Journal with the same name as the original folder. + * @param {Folder} folder source folder to merge + */ +async function mergeJournalFolderIntoSingleJournal(folder) { + const newJournal = await JournalEntry.create({name: folder.name, folder: folder.folder}); + folder.contents.forEach(journal => { + journal.pages.forEach(page => { + const pageData = { + name: `${journal.name} - ${page.name}`, + type: page.type, + src: page.src + } + + switch (page.type) { + case "text": + Object.entries(flattenObject(page.text)).forEach(([key, val]) => pageData[`text.${key}`] = val ); + break; + + case "image": + Object.entries(flattenObject(page.image)).forEach(([key, val]) => pageData[`text.${key}`] = val ); + break; + + case "video": + Object.entries(flattenObject(page.video)).forEach(([key, val]) => pageData[`text.${key}`] = val ); + break; + } + + // Create a single page in the new journal with old page data + JournalEntryPage.create(pageData, {parent: newJournal}); + }); + }); +} + +// Add Split Journal to the entries +Hooks.on('getJournalSheetEntryContext', handleSplitPageContextMenu); +Hooks.on('getJournalDirectoryEntryContext', handleJournalContextMenu); +Hooks.on('getJournalDirectoryFolderContext', handleJournalFolderContextMenu); diff --git a/templates/split-journal-dialog.html b/templates/split-journal-dialog.html new file mode 100644 index 0000000..46e8e9a --- /dev/null +++ b/templates/split-journal-dialog.html @@ -0,0 +1,9 @@ +
+
+

{{localize "SPLITJOINJNL.SPLITJOURNAL.folderNameDesc"}}

+
+
+ + +
+
diff --git a/templates/split-journal-page-dialog.html b/templates/split-journal-page-dialog.html new file mode 100644 index 0000000..5c0c6eb --- /dev/null +++ b/templates/split-journal-page-dialog.html @@ -0,0 +1,19 @@ +
+
+ + +
+
+

{{localize "SPLITJOINJNL.SPLITPAGE.journalNameDesc"}}

+
+
+ + +
+