From bc13cbdb79fc4463f7aa66c27d8f4677178830f1 Mon Sep 17 00:00:00 2001 From: Markus Koskimies Date: Sun, 13 Oct 2024 20:55:44 +0300 Subject: [PATCH] Implementing "act" block type (#242) Working with "act" block type - Fixed folding - Fixed foldByTags() - Fixed word table creation - Fixed Story Arc View - Fixed exports, sort of... ...but some of them are pretty crude. Need to fix them more. - Fixed save - Fixed chapter DropZones - DnD fixed - Fixed import - Fixed loading - Single act not shown in index. There is no need, there is chapter DropZone anyways. - Export fixes - Package updates - Fixing index current indicator - Need more performance to withIDs(). This is becoming a bottleneck. Need to figure out something. --- examples/ActTest.mawe | 74 ++++++++ examples/export/ExportTest.mawe | 101 +++++++++++ examples/migration/Story1.v3.mawe | 16 ++ package-lock.json | 98 +++++------ package.json | 4 +- src/document/util.js | 61 ++++--- src/document/xmljs/load.js | 95 +++++++--- src/document/xmljs/migration.js | 54 +++++- src/document/xmljs/save.js | 38 +++- src/gui/app/app.js | 7 +- src/gui/app/views.js | 1 - src/gui/arc/arc.js | 92 ++++++---- src/gui/common/docIndex.js | 139 ++++++++++----- src/gui/common/factory.js | 8 +- src/gui/common/styles/TOC.css | 46 ++++- src/gui/common/styles/sheet.css | 57 +++--- src/gui/editor/editor.js | 59 ++++--- src/gui/editor/slateEditor.js | 284 ++++++++++++++++++------------ src/gui/editor/slateHelpers.js | 96 ++++++---- src/gui/export/export.js | 35 +++- src/gui/export/formatDoc.js | 69 ++++++-- src/gui/export/formatHTML.js | 15 ++ src/gui/export/formatRTF.js | 19 +- src/gui/export/formatTEX.js | 24 ++- src/gui/export/formatTXT.js | 32 +++- src/gui/import/import.js | 4 +- src/gui/import/importText.js | 48 ++++- src/gui/import/preview.js | 19 +- 28 files changed, 1140 insertions(+), 455 deletions(-) create mode 100644 examples/ActTest.mawe create mode 100644 examples/export/ExportTest.mawe create mode 100644 examples/migration/Story1.v3.mawe diff --git a/examples/ActTest.mawe b/examples/ActTest.mawe new file mode 100644 index 00000000..ec131b2d --- /dev/null +++ b/examples/ActTest.mawe @@ -0,0 +1,74 @@ + + + + + + + + + +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque sagittis faucibus odio, sed fringilla lacus tempor eu. Curabitur lacinia ante quis urna placerat, vitae ullamcorper dolor accumsan. Nam ex velit, dictum eget porttitor vitae, aliquet at tortor. Vivamus dictum mauris ut dolor mattis, ut pulvinar ligula scelerisque. Vivamus luctus neque nec urna sodales fringilla. Ut gravida nibh risus, ac tempus mauris scelerisque nec. Vivamus semper erat eget placerat imperdiet. Fusce non lorem eu diam vulputate porta non eu nibh. Mauris egestas est tellus, id placerat libero tempus et. Integer eget ultrices ante. Vestibulum est arcu, elementum a ornare convallis, fringilla.

+
+ +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque sagittis faucibus odio, sed fringilla lacus tempor eu. Curabitur lacinia ante quis urna placerat, vitae ullamcorper dolor accumsan. Nam ex velit, dictum eget porttitor vitae, aliquet at tortor. Vivamus dictum mauris ut dolor mattis, ut pulvinar ligula scelerisque. Vivamus luctus neque nec urna sodales fringilla. Ut gravida nibh risus, ac tempus mauris scelerisque nec. Vivamus semper erat eget placerat imperdiet. Fusce non lorem eu diam vulputate porta non eu nibh. Mauris egestas est tellus, id placerat libero tempus et. Integer eget ultrices ante. Vestibulum est arcu, elementum a ornare convallis, fringilla.

+
+
+ + +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque sagittis faucibus odio, sed fringilla lacus tempor eu. Curabitur lacinia ante quis urna placerat, vitae ullamcorper dolor accumsan. Nam ex velit, dictum eget porttitor vitae, aliquet at tortor. Vivamus dictum mauris ut dolor mattis, ut pulvinar ligula scelerisque. Vivamus luctus neque nec urna sodales fringilla. Ut gravida nibh risus, ac tempus mauris scelerisque nec. Vivamus semper erat eget placerat imperdiet. Fusce non lorem eu diam vulputate porta non eu nibh. Mauris egestas est tellus, id placerat libero tempus et. Integer eget ultrices ante. Vestibulum est arcu, elementum a ornare convallis, fringilla.

+
+
+
+ + + +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque sagittis faucibus odio, sed fringilla lacus tempor eu. Curabitur lacinia ante quis urna placerat, vitae ullamcorper dolor accumsan. Nam ex velit, dictum eget porttitor vitae, aliquet at tortor. Vivamus dictum mauris ut dolor mattis, ut pulvinar ligula scelerisque. Vivamus luctus neque nec urna sodales fringilla. Ut gravida nibh risus, ac tempus mauris scelerisque nec. Vivamus semper erat eget placerat imperdiet. Fusce non lorem eu diam vulputate porta non eu nibh. Mauris egestas est tellus, id placerat libero tempus et. Integer eget ultrices ante. Vestibulum est arcu, elementum a ornare convallis, fringilla.

+
+
+ + +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque sagittis faucibus odio, sed fringilla lacus tempor eu. Curabitur lacinia ante quis urna placerat, vitae ullamcorper dolor accumsan. Nam ex velit, dictum eget porttitor vitae, aliquet at tortor. Vivamus dictum mauris ut dolor mattis, ut pulvinar ligula scelerisque. Vivamus luctus neque nec urna sodales fringilla. Ut gravida nibh risus, ac tempus mauris scelerisque nec. Vivamus semper erat eget placerat imperdiet. Fusce non lorem eu diam vulputate porta non eu nibh. Mauris egestas est tellus, id placerat libero tempus et. Integer eget ultrices ante. Vestibulum est arcu, elementum a ornare convallis, fringilla.

+
+
+
+ + + + + + +
+
+
+
+
+ + + + + + + + + + + + + +
\ No newline at end of file diff --git a/examples/export/ExportTest.mawe b/examples/export/ExportTest.mawe new file mode 100644 index 00000000..7a10d7d5 --- /dev/null +++ b/examples/export/ExportTest.mawe @@ -0,0 +1,101 @@ + + + + + + + + + +

Preface. Unnamed, non-numbered chapters are omitted.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque sagittis faucibus odio, sed fringilla lacus tempor eu. Curabitur lacinia ante quis urna placerat, vitae ullamcorper dolor accumsan. Nam ex velit, dictum eget porttitor vitae, aliquet at tortor. Vivamus dictum mauris ut dolor mattis, ut pulvinar ligula scelerisque. Vivamus luctus neque nec urna sodales fringilla. Ut gravida nibh risus, ac tempus mauris scelerisque nec. Vivamus semper erat eget placerat imperdiet. Fusce non lorem eu diam vulputate porta non eu nibh. Mauris egestas est tellus, id placerat libero tempus et. Integer eget ultrices ante. Vestibulum est arcu, elementum a ornare convallis, fringilla.

+
+
+
+ + +

Unnumbered chapter as a prologue, before first part.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque sagittis faucibus odio, sed fringilla lacus tempor eu. Curabitur lacinia ante quis urna placerat, vitae ullamcorper dolor accumsan. Nam ex velit, dictum eget porttitor vitae, aliquet at tortor. Vivamus dictum mauris ut dolor mattis, ut pulvinar ligula scelerisque. Vivamus luctus neque nec urna sodales fringilla. Ut gravida nibh risus, ac tempus mauris scelerisque nec. Vivamus semper erat eget placerat imperdiet. Fusce non lorem eu diam vulputate porta non eu nibh. Mauris egestas est tellus, id placerat libero tempus et. Integer eget ultrices ante. Vestibulum est arcu, elementum a ornare convallis, fringilla.

+
+
+
+
+ + + +

Preface. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque sagittis faucibus odio, sed fringilla lacus tempor eu. Curabitur lacinia ante quis urna placerat, vitae ullamcorper dolor accumsan. Nam ex velit, dictum eget porttitor vitae, aliquet at tortor. Vivamus dictum mauris ut dolor mattis, ut pulvinar ligula scelerisque. Vivamus luctus neque nec urna sodales fringilla. Ut gravida nibh risus, ac tempus mauris scelerisque nec. Vivamus semper erat eget placerat imperdiet. Fusce non lorem eu diam vulputate porta non eu nibh. Mauris egestas est tellus, id placerat libero tempus et. Integer eget ultrices ante. Vestibulum est arcu, elementum a ornare convallis, fringilla.

+
+
+ + +

First chapter, first scene.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque sagittis faucibus odio, sed fringilla lacus tempor eu. Curabitur lacinia ante quis urna placerat, vitae ullamcorper dolor accumsan. Nam ex velit, dictum eget porttitor vitae, aliquet at tortor. Vivamus dictum mauris ut dolor mattis, ut pulvinar ligula scelerisque. Vivamus luctus neque nec urna sodales fringilla. Ut gravida nibh risus, ac tempus mauris scelerisque nec. Vivamus semper erat eget placerat imperdiet. Fusce non lorem eu diam vulputate porta non eu nibh. Mauris egestas est tellus, id placerat libero tempus et. Integer eget ultrices ante. Vestibulum est arcu, elementum a ornare convallis, fringilla.

+
+ +

Second scene.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque sagittis faucibus odio, sed fringilla lacus tempor eu. Curabitur lacinia ante quis urna placerat, vitae ullamcorper dolor accumsan. Nam ex velit, dictum eget porttitor vitae, aliquet at tortor. Vivamus dictum mauris ut dolor mattis, ut pulvinar ligula scelerisque. Vivamus luctus neque nec urna sodales fringilla. Ut gravida nibh risus, ac tempus mauris scelerisque nec. Vivamus semper erat eget placerat imperdiet. Fusce non lorem eu diam vulputate porta non eu nibh. Mauris egestas est tellus, id placerat libero tempus et. Integer eget ultrices ante. Vestibulum est arcu, elementum a ornare convallis, fringilla.

+
+
+ + +

Unnumbered chapter between.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque sagittis faucibus odio, sed fringilla lacus tempor eu. Curabitur lacinia ante quis urna placerat, vitae ullamcorper dolor accumsan. Nam ex velit, dictum eget porttitor vitae, aliquet at tortor. Vivamus dictum mauris ut dolor mattis, ut pulvinar ligula scelerisque. Vivamus luctus neque nec urna sodales fringilla. Ut gravida nibh risus, ac tempus mauris scelerisque nec. Vivamus semper erat eget placerat imperdiet. Fusce non lorem eu diam vulputate porta non eu nibh. Mauris egestas est tellus, id placerat libero tempus et. Integer eget ultrices ante. Vestibulum est arcu, elementum a ornare convallis, fringilla.

+
+
+
+
+ + + +

Chapter 2.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque sagittis faucibus odio, sed fringilla lacus tempor eu. Curabitur lacinia ante quis urna placerat, vitae ullamcorper dolor accumsan. Nam ex velit, dictum eget porttitor vitae, aliquet at tortor. Vivamus dictum mauris ut dolor mattis, ut pulvinar ligula scelerisque. Vivamus luctus neque nec urna sodales fringilla. Ut gravida nibh risus, ac tempus mauris scelerisque nec. Vivamus semper erat eget placerat imperdiet. Fusce non lorem eu diam vulputate porta non eu nibh. Mauris egestas est tellus, id placerat libero tempus et. Integer eget ultrices ante. Vestibulum est arcu, elementum a ornare convallis, fringilla.

+
+
+
+ + +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque sagittis faucibus odio, sed fringilla lacus tempor eu. Curabitur lacinia ante quis urna placerat, vitae ullamcorper dolor accumsan. Nam ex velit, dictum eget porttitor vitae, aliquet at tortor. Vivamus dictum mauris ut dolor mattis, ut pulvinar ligula scelerisque. Vivamus luctus neque nec urna sodales fringilla. Ut gravida nibh risus, ac tempus mauris scelerisque nec. Vivamus semper erat eget placerat imperdiet. Fusce non lorem eu diam vulputate porta non eu nibh. Mauris egestas est tellus, id placerat libero tempus et. Integer eget ultrices ante. Vestibulum est arcu, elementum a ornare convallis, fringilla.

+
+
+
+
+ + + + + + +
+
+
+
+
+ + + + + + + + + + + + +
\ No newline at end of file diff --git a/examples/migration/Story1.v3.mawe b/examples/migration/Story1.v3.mawe new file mode 100644 index 00000000..68cf1953 --- /dev/null +++ b/examples/migration/Story1.v3.mawe @@ -0,0 +1,16 @@ + + +Story v3/1 + + + + + + +

If loaded correctly, story title is "Story".

+
+
+
+ + + diff --git a/package-lock.json b/package-lock.json index 2169abdf..9ca4d84b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-infinite-scroll-component": "^6.1.0", - "recharts": "^2.12.7", + "recharts": "^2.13.0", "slate": "^0.103.0", "slate-history": "^0.109.0", "slate-react": "^0.110.1", @@ -44,7 +44,7 @@ "concurrently": "^9.0.1", "cross-env": "^7.0.3", "electron": "^32.2.0", - "electron-builder": "^25.1.7", + "electron-builder": "^25.1.8", "electron-reload": "^2.0.0-alpha.1", "nodemon": "^3.1.7", "react-scripts": "^5.0.1", @@ -5959,9 +5959,9 @@ "dev": true }, "node_modules/app-builder-lib": { - "version": "25.1.7", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-25.1.7.tgz", - "integrity": "sha512-JxmN+D/Dn7BLQoN+cTFO+zbMHcpI10v/xjyjFO1FKpHbApOG+OQt/xUyVjKWp4FYplIfuHdpxqTXo1PN/Wzm/A==", + "version": "25.1.8", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-25.1.8.tgz", + "integrity": "sha512-pCqe7dfsQFBABC1jeKZXQWhGcCPF3rPCXDdfqVKjIeWBcXzyC1iOWZdfFhGl+S9MyE/k//DFmC6FzuGAUudNDg==", "dev": true, "dependencies": { "@develar/schema-utils": "~2.6.5", @@ -6001,8 +6001,8 @@ "node": ">=14.0.0" }, "peerDependencies": { - "dmg-builder": "25.1.7", - "electron-builder-squirrel-windows": "25.1.7" + "dmg-builder": "25.1.8", + "electron-builder-squirrel-windows": "25.1.8" } }, "node_modules/app-builder-lib/node_modules/dotenv": { @@ -9073,12 +9073,12 @@ "dev": true }, "node_modules/dmg-builder": { - "version": "25.1.7", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-25.1.7.tgz", - "integrity": "sha512-Hac0AfXQrAV62JT99Had6bvUJb/f7vjJTaLOsmA/gAQcrc/cLmNAqCJ0ZZDqwKy2+LKXnxx45TvMXvovKd4iMg==", + "version": "25.1.8", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-25.1.8.tgz", + "integrity": "sha512-NoXo6Liy2heSklTI5OIZbCgXC1RzrDQsZkeEwXhdOro3FT1VBOvbubvscdPnjVuQ4AMwwv61oaH96AbiYg9EnQ==", "dev": true, "dependencies": { - "app-builder-lib": "25.1.7", + "app-builder-lib": "25.1.8", "builder-util": "25.1.7", "builder-util-runtime": "9.2.10", "fs-extra": "^10.1.0", @@ -9366,16 +9366,16 @@ } }, "node_modules/electron-builder": { - "version": "25.1.7", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-25.1.7.tgz", - "integrity": "sha512-lsKtX93GSHWnmuteNRvBzgJIjRiiYB0qrJVRjShwBi75Ns+mRdWeOGZiXItqOWj+3g5UyY722kgoq2WlqCB87A==", + "version": "25.1.8", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-25.1.8.tgz", + "integrity": "sha512-poRgAtUHHOnlzZnc9PK4nzG53xh74wj2Jy7jkTrqZ0MWPoHGh1M2+C//hGeYdA+4K8w4yiVCNYoLXF7ySj2Wig==", "dev": true, "dependencies": { - "app-builder-lib": "25.1.7", + "app-builder-lib": "25.1.8", "builder-util": "25.1.7", "builder-util-runtime": "9.2.10", "chalk": "^4.1.2", - "dmg-builder": "25.1.7", + "dmg-builder": "25.1.8", "fs-extra": "^10.1.0", "is-ci": "^3.0.0", "lazy-val": "^1.0.5", @@ -9391,13 +9391,13 @@ } }, "node_modules/electron-builder-squirrel-windows": { - "version": "25.1.7", - "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-25.1.7.tgz", - "integrity": "sha512-nJMvw1FNy+6YP8HmjSb0JwMowpdlZpydZGab9KevKO/fIC9wTcr5rkhbLsTfEPOjdAqOTycRoK0mOJCFB/1uig==", + "version": "25.1.8", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-25.1.8.tgz", + "integrity": "sha512-2ntkJ+9+0GFP6nAISiMabKt6eqBB0kX1QqHNWFWAXgi0VULKGisM46luRFpIBiU3u/TDmhZMM8tzvo2Abn3ayg==", "dev": true, "peer": true, "dependencies": { - "app-builder-lib": "25.1.7", + "app-builder-lib": "25.1.8", "archiver": "^5.3.1", "builder-util": "25.1.7", "fs-extra": "^10.1.0" @@ -18198,14 +18198,14 @@ } }, "node_modules/recharts": { - "version": "2.12.7", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.7.tgz", - "integrity": "sha512-hlLJMhPQfv4/3NBSAyq3gzGg4h2v69RJh6KU7b3pXYNNAELs9kEoXOjbkxdXpALqKBoVmVptGfLpxdaVYqjmXQ==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.13.0.tgz", + "integrity": "sha512-sbfxjWQ+oLWSZEWmvbq/DFVdeRLqqA6d0CDjKx2PkxVVdoXo16jvENCE+u/x7HxOO+/fwx//nYRwb8p8X6s/lQ==", "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", - "react-is": "^16.10.2", + "react-is": "^18.3.1", "react-smooth": "^4.0.0", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", @@ -18235,11 +18235,6 @@ "node": ">=6" } }, - "node_modules/recharts/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, "node_modules/recursive-readdir": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", @@ -26681,9 +26676,9 @@ "dev": true }, "app-builder-lib": { - "version": "25.1.7", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-25.1.7.tgz", - "integrity": "sha512-JxmN+D/Dn7BLQoN+cTFO+zbMHcpI10v/xjyjFO1FKpHbApOG+OQt/xUyVjKWp4FYplIfuHdpxqTXo1PN/Wzm/A==", + "version": "25.1.8", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-25.1.8.tgz", + "integrity": "sha512-pCqe7dfsQFBABC1jeKZXQWhGcCPF3rPCXDdfqVKjIeWBcXzyC1iOWZdfFhGl+S9MyE/k//DFmC6FzuGAUudNDg==", "dev": true, "requires": { "@develar/schema-utils": "~2.6.5", @@ -28980,12 +28975,12 @@ "dev": true }, "dmg-builder": { - "version": "25.1.7", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-25.1.7.tgz", - "integrity": "sha512-Hac0AfXQrAV62JT99Had6bvUJb/f7vjJTaLOsmA/gAQcrc/cLmNAqCJ0ZZDqwKy2+LKXnxx45TvMXvovKd4iMg==", + "version": "25.1.8", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-25.1.8.tgz", + "integrity": "sha512-NoXo6Liy2heSklTI5OIZbCgXC1RzrDQsZkeEwXhdOro3FT1VBOvbubvscdPnjVuQ4AMwwv61oaH96AbiYg9EnQ==", "dev": true, "requires": { - "app-builder-lib": "25.1.7", + "app-builder-lib": "25.1.8", "builder-util": "25.1.7", "builder-util-runtime": "9.2.10", "dmg-license": "^1.0.11", @@ -29214,16 +29209,16 @@ } }, "electron-builder": { - "version": "25.1.7", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-25.1.7.tgz", - "integrity": "sha512-lsKtX93GSHWnmuteNRvBzgJIjRiiYB0qrJVRjShwBi75Ns+mRdWeOGZiXItqOWj+3g5UyY722kgoq2WlqCB87A==", + "version": "25.1.8", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-25.1.8.tgz", + "integrity": "sha512-poRgAtUHHOnlzZnc9PK4nzG53xh74wj2Jy7jkTrqZ0MWPoHGh1M2+C//hGeYdA+4K8w4yiVCNYoLXF7ySj2Wig==", "dev": true, "requires": { - "app-builder-lib": "25.1.7", + "app-builder-lib": "25.1.8", "builder-util": "25.1.7", "builder-util-runtime": "9.2.10", "chalk": "^4.1.2", - "dmg-builder": "25.1.7", + "dmg-builder": "25.1.8", "fs-extra": "^10.1.0", "is-ci": "^3.0.0", "lazy-val": "^1.0.5", @@ -29255,13 +29250,13 @@ } }, "electron-builder-squirrel-windows": { - "version": "25.1.7", - "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-25.1.7.tgz", - "integrity": "sha512-nJMvw1FNy+6YP8HmjSb0JwMowpdlZpydZGab9KevKO/fIC9wTcr5rkhbLsTfEPOjdAqOTycRoK0mOJCFB/1uig==", + "version": "25.1.8", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-25.1.8.tgz", + "integrity": "sha512-2ntkJ+9+0GFP6nAISiMabKt6eqBB0kX1QqHNWFWAXgi0VULKGisM46luRFpIBiU3u/TDmhZMM8tzvo2Abn3ayg==", "dev": true, "peer": true, "requires": { - "app-builder-lib": "25.1.7", + "app-builder-lib": "25.1.8", "archiver": "^5.3.1", "builder-util": "25.1.7", "fs-extra": "^10.1.0" @@ -35743,14 +35738,14 @@ } }, "recharts": { - "version": "2.12.7", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.7.tgz", - "integrity": "sha512-hlLJMhPQfv4/3NBSAyq3gzGg4h2v69RJh6KU7b3pXYNNAELs9kEoXOjbkxdXpALqKBoVmVptGfLpxdaVYqjmXQ==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.13.0.tgz", + "integrity": "sha512-sbfxjWQ+oLWSZEWmvbq/DFVdeRLqqA6d0CDjKx2PkxVVdoXo16jvENCE+u/x7HxOO+/fwx//nYRwb8p8X6s/lQ==", "requires": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", - "react-is": "^16.10.2", + "react-is": "^18.3.1", "react-smooth": "^4.0.0", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", @@ -35761,11 +35756,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==" - }, - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" } } }, diff --git a/package.json b/package.json index 335f9cc7..9d20e1f0 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-infinite-scroll-component": "^6.1.0", - "recharts": "^2.12.7", + "recharts": "^2.13.0", "slate": "^0.103.0", "slate-history": "^0.109.0", "slate-react": "^0.110.1", @@ -57,7 +57,7 @@ "concurrently": "^9.0.1", "cross-env": "^7.0.3", "electron": "^32.2.0", - "electron-builder": "^25.1.7", + "electron-builder": "^25.1.8", "electron-reload": "^2.0.0-alpha.1", "nodemon": "^3.1.7", "react-scripts": "^5.0.1", diff --git a/src/document/util.js b/src/document/util.js index 82032025..a947044f 100644 --- a/src/document/util.js +++ b/src/document/util.js @@ -78,7 +78,7 @@ export function createDateStamp(date) { //----------------------------------------------------------------------------- export function filterCtrlElems(blocks) { - const ctrltypes = ["hchapter", "hscene"] + const ctrltypes = ["hact", "hchapter", "hscene"] return blocks.filter(block => !ctrltypes.includes(block.type)) } @@ -92,26 +92,27 @@ export function elemAsText(elem) { } export function elemHeading(elem) { - if(elem.type === "chapter") { - if(elem.children.length && elem.children[0].type === "hchapter") { - return elem.children[0]; - } - return undefined - } - if(elem.type === "scene") { - if(elem.children.length && elem.children[0].type === "hscene") { - return elem.children[0]; + + const [first] = elem.children ?? [] + if(first) { + if( + (elem.type === "act" && first.type === "hact") || + (elem.type === "chapter" && first.type === "hchapter") || + (elem.type === "scene" && first.type === "hscene") + ) { + return first } - return undefined } + + return undefined } export function elemName(elem) { return elemAsText(elemHeading(elem)) } -export function elemUnnumbered(elem) { - return elemHeading(elem)?.unnumbered +export function elemNumbered(elem) { + return elemHeading(elem)?.numbered } //----------------------------------------------------------------------------- @@ -141,16 +142,17 @@ export function wordcount(text) { export function createWordTable(section) { const wt = new Map() - for(const chapter of section.chapters) { - for(const scene of chapter.children) { - for(const p of scene.children) { - if(p.type !== "p") continue - for(const word of text2words(elemAsText(p))) { - const lowcase = word.toLowerCase() - const count = wt.has(lowcase) ? wt.get(lowcase) : 0 - wt.set(lowcase, count + 1) + for(const act of section.acts) { + for(const chapter of filterCtrlElems(act.children)) { + for(const scene of filterCtrlElems(chapter.children)) { + for(const p of scene.children) { + if(p.type !== "p") continue + for(const word of text2words(elemAsText(p))) { + const lowcase = word.toLowerCase() + const count = wt.has(lowcase) ? wt.get(lowcase) : 0 + wt.set(lowcase, count + 1) + } } - } } } @@ -165,12 +167,14 @@ export function createWordTable(section) { export function createTagTable(section) { const tags = new Set() - for(const chapter of section.chapters) { - for(const scene of chapter.children) { - for(const p of scene.children) { - const keys = elemTags(p) - for(const key of keys) { - tags.add(key); + for(const act of section.acts) { + for(const chapter of filterCtrlElems(act.children)) { + for(const scene of filterCtrlElems(chapter.children)) { + for(const p of scene.children) { + const keys = elemTags(p) + for(const key of keys) { + tags.add(key); + } } } } @@ -213,6 +217,7 @@ export function wcElem(elem) { switch(elem.type) { case "sect": + case "act": case "chapter": case "scene": return wcChildren(elem.children) diff --git a/src/document/xmljs/load.js b/src/document/xmljs/load.js index 311dc363..fb52098b 100644 --- a/src/document/xmljs/load.js +++ b/src/document/xmljs/load.js @@ -153,41 +153,73 @@ function parseHead(head, history) { //***************************************************************************** function parseSection(section) { - function getChapters() { - const chapters = elemFindall(section, "chapter") - if(!chapters.length) return [{type: "chapter", id: nanoid()}] - return chapters + function getActs() { + const acts = elemFindall(section, "act") + if(!acts.length) return [{type: "element", name: "act"}] + return acts } - const chapters = getChapters().map(parseChapter) - const words = wcChildren(chapters) + const acts = getActs().map(parseAct) + const words = wcChildren(acts) return { type: "sect", - chapters, + acts, words, } } -function parseChapter(chapter, index) { - const {name, folded, unnumbered} = chapter.attributes ?? {}; +function parseAct(act, index) { + if(act.type !== "element" || act.name !== "act") { + console.log("Invalid act:", act) + throw new Error("Invalid act", act) + } + const {name, folded, numbered} = act.attributes ?? {}; const header = (!index && !name) ? [] : [{ - type: "hchapter", + type: "hact", id: nanoid(), - unnumbered: unnumbered ? true : undefined, + numbered, children: [{text: name ?? ""}], words: {} }] - const empty = [{ - type: "scene", + const empty = [{type: "element", name: "chapter"}] + const elements = act.elements?.length ? act.elements : empty + + const children = elements.map(parseChapter) + const words = wcChildren(children) + + return { + type: "act", id: nanoid(), - children: [] + folded: folded ? true : undefined, + children: [ + ...header, + ...children, + ], + words + } +} + +function parseChapter(chapter, index) { + if(chapter.type !== "element" || chapter.name !== "chapter") { + console.log("Invalid chapter:", chapter) + throw new Error("Invalid chapter:", chapter) + } + const {name, folded, numbered} = chapter.attributes ?? {}; + const header = (!index && !name) ? [] : [{ + type: "hchapter", + id: nanoid(), + numbered: numbered, + children: [{text: name ?? ""}], + words: {} }] - const children = (chapter.elements ?? empty).map(parseScene) + const empty = [{type: "element", name: "scene"}] + const elements = chapter.elements?.length ? chapter.elements : empty + + const children = elements.map(parseScene) const words = wcChildren(children) return { type: "chapter", id: nanoid(), - //name, folded: folded ? true : undefined, children: [ ...header, @@ -198,6 +230,10 @@ function parseChapter(chapter, index) { } function parseScene(scene, index) { + if(scene.type !== "element" || scene.name !== "scene") { + console.log("Invalid scene:", scene) + throw new Error("Invalid scene", scene) + } const {name, folded} = scene.attributes ?? {}; const header = (!index && !name) ? [] : [{ type: "hscene", @@ -205,12 +241,10 @@ function parseScene(scene, index) { children: [{text: name ?? ""}], words: {} }] - const empty = [{ - type: "p", - id: nanoid(), - children: [{text: ""}] - }] - const children = (scene.elements ?? empty).map(parseParagraph).map(elem => ({...elem, words: wcElem(elem)})) + const empty = [{type: "element", name: "p", children: []}] + const elements = scene.elements?.length ? scene.elements : empty + + const children = elements.map(parseParagraph).map(elem => ({...elem, words: wcElem(elem)})) const words = wcChildren(children) return { @@ -229,11 +263,22 @@ function parseScene(scene, index) { //--------------------------------------------------------------------------- function parseParagraph(elem, index) { + if(elem.type !== "element") { + console.log("Invalid paragraph:", elem) + throw new Error("Invalid paragraph", elem) + } //console.log(elem) - const {name, type} = elem + const {name} = elem + + const empty = [{ + type: "element", + name: "p", + children: [{type: "text", text: ""}] + }] + const elements = elem.elements?.length ? elem.elements : empty - const children = elem.elements?.map(e => parseMarks(e, {})).flat() ?? [{text: ""}] + const children = elements.map(e => parseMarks(e, {})).flat() const text = children.map(child => child.text).join("") @@ -241,7 +286,7 @@ function parseParagraph(elem, index) { //console.log(text) return { - type: (type === "element" && name === "p" && !text) ? "br" : (name ?? type), + type: (name === "p" && !text) ? "br" : name, id: nanoid(), children } diff --git a/src/document/xmljs/migration.js b/src/document/xmljs/migration.js index 1199451c..8c7d902d 100644 --- a/src/document/xmljs/migration.js +++ b/src/document/xmljs/migration.js @@ -23,7 +23,7 @@ import { elemFind, elemFindall, elem2Text } from "./tree"; // //----------------------------------------------------------------------------- -const supported = ["1", "2", "3"] +const supported = ["1", "2", "3", "4"] export function migrate(root) { @@ -41,6 +41,7 @@ export function migrate(root) { v2_fixes, v2_to_v3, v3_fixes, + v3_to_v4, ].reduce((story, func) => func(story), story) } @@ -63,10 +64,7 @@ function v1_to_v2(story) { return { ...story, - attributes: { - ...story.attributes, - version: "2", - } + attributes: {...story.attributes, version: "2" } } } @@ -137,11 +135,11 @@ function v2_to_v3(story) { return { ...story, + attributes: {...story.attributes, version: "3"}, elements: (story.elements ?? []) .filter(elem => elem.name !== "body") .filter(elem => elem.name !== "notes") .concat([body, notes]), - attributes: {...story.attributes, version: "3"} } } @@ -214,3 +212,47 @@ function v3_fix_exports(exportElem) { } } } + +//***************************************************************************** +// +// v3 --> v4 +// +// - Body/notes --> act +// +//***************************************************************************** + +function v3_to_v4(story) { + + const {version} = story.attributes ?? {} + + if(version !== "3") return story + + console.log("Migrate v3 -> v4") + + // Fix unnumbered --> numbered + const bodyElem = elemFind(story, "body") ?? {type: "element", name: "body", elements: []} + const notesElem = elemFind(story, "notes") ?? {type: "element", name: "notes", elements: []} + + return { + ...story, + attributes: {...story.attributes, version: "4"}, + elements: [ + ...story.elements + .filter(elem => elem.name !== "body") + .filter(elem => elem.name !== "notes"), + wrap(bodyElem), + wrap(notesElem) + ] + } + + function wrap(elem) { + const {elements} = elem + return { + ...elem, + elements: [{ + type: "element", name: "act", + elements + }] + } + } +} diff --git a/src/document/xmljs/save.js b/src/document/xmljs/save.js index 20c80073..f6c22138 100644 --- a/src/document/xmljs/save.js +++ b/src/document/xmljs/save.js @@ -10,7 +10,7 @@ import { saveViewSettings } from "../../gui/app/views"; import { saveChartSettings } from "../../gui/arc/arc"; import { saveEditorSettings } from "../../gui/editor/editor"; import {saveExportSettings} from "../../gui/export/export"; -import {uuid as getUUID, buf2file, elemName, filterCtrlElems, elemUnnumbered} from "../util"; +import {uuid as getUUID, buf2file, elemName, filterCtrlElems, elemNumbered} from "../util"; //---------------------------------------------------------------------------- @@ -36,7 +36,7 @@ export function toXML(doc) { attributes: { uuid: doc.uuid ?? getUUID(), format: "mawe", - version: "3", + version: "4", name: doc.head?.name } }, @@ -107,20 +107,42 @@ function toExport(exports) { function toBody(body) { - const {chapters} = body; + const {acts} = body; return xmlLines( {type: "body"}, - ...chapters.map(toChapter), + ...acts.map(toAct), ) } function toNotes(notes) { - const {chapters} = notes; + const {acts} = notes; return xmlLines( {type: "notes"}, - ...chapters.map(toChapter) + ...acts.map(toAct) + ) +} + +//----------------------------------------------------------------------------- +// Acts +//----------------------------------------------------------------------------- + +function toAct(act) { + const {folded} = act; + const name = elemName(act) + const numbered = elemNumbered(act) + + return xmlLines( + { + type: "act", + attributes: { + name: name, + folded: folded ? true : undefined, + numbered: numbered ? true : undefined, + }, + }, + ...filterCtrlElems(act.children).map(toChapter), ) } @@ -131,7 +153,7 @@ function toNotes(notes) { function toChapter(chapter) { const {folded} = chapter; const name = elemName(chapter) - const unnumbered = elemUnnumbered(chapter) + const numbered = elemNumbered(chapter) return xmlLines( { @@ -139,7 +161,7 @@ function toChapter(chapter) { attributes: { name: name, folded: folded ? true : undefined, - unnumbered: unnumbered ? true : undefined, + numbered: numbered ? true : undefined, }, }, ...filterCtrlElems(chapter.children).map(toScene), diff --git a/src/gui/app/app.js b/src/gui/app/app.js index e9d748b2..d1932a89 100644 --- a/src/gui/app/app.js +++ b/src/gui/app/app.js @@ -118,7 +118,8 @@ export default function App(props) { setCommand({ action: "import", //file: {id: "./examples/Frankenstein.txt", name: "Frankenstein.txt" }, ext: ".txt", - file: {id: "./examples/Frankenstein.md", name: "Frankenstein.md" }, ext: ".md", + //file: {id: "./examples/Frankenstein.md", name: "Frankenstein.md" }, ext: ".md", + file: {id: "./local/Maankutsuja/Maankutsuja2.docx", name: "Maankutsuja2.docx" }, ext: ".docx", }) /**/ }, []) @@ -291,8 +292,9 @@ function WithDoc({setCommand, doc, updateDoc, recent}) { - + + @@ -300,6 +302,7 @@ function WithDoc({setCommand, doc, updateDoc, recent}) { {/* */} + diff --git a/src/gui/app/views.js b/src/gui/app/views.js index 2b89db23..bb45a2e1 100644 --- a/src/gui/app/views.js +++ b/src/gui/app/views.js @@ -18,7 +18,6 @@ import { SingleEditView } from "../editor/editor"; import { StoryArc } from "../arc/arc" import { Stats } from "../stats/stats" import { Export } from "../export/export" -import {ImportView} from "../import/import"; //***************************************************************************** // diff --git a/src/gui/arc/arc.js b/src/gui/arc/arc.js index ae28f50e..cb43362b 100644 --- a/src/gui/arc/arc.js +++ b/src/gui/arc/arc.js @@ -36,7 +36,7 @@ import {elemName, filterCtrlElems, mawe} from "../../document"; export function loadChartSettings(settings) { // TODO: Check that fields have valid values (table keys) return { - elements: "scenes", + elements: "scene", template: "beatsheet", mode: "topCCW", ...(settings?.attributes ?? {}) @@ -80,7 +80,7 @@ export function StoryArc({doc, updateDoc}) { const settings = { elements: { buttons: elemButtons, - choices: ["chapters", "scenes"], + choices: ["act", "chapter", "scene"], selected: doc.ui.arc.elements, setSelected: setElements, exclusive: true, @@ -102,9 +102,10 @@ export function StoryArc({doc, updateDoc}) { } } - function selectInclude() { + function indexElements() { switch(doc.ui.arc.elements) { - case "chapters": return ["chapter"] + case "act": return [] + case "chapter": return ["chapter"] } return ["chapter", "scene"] } @@ -116,7 +117,7 @@ export function StoryArc({doc, updateDoc}) { @@ -171,6 +172,12 @@ function ChartView({settings, doc, updateDoc}) { const section = doc.body + //--------------------------------------------------------------------------- + // Data selection + //--------------------------------------------------------------------------- + + const data = flatSection(section).filter(e => e.type === doc.ui.arc.elements) + //--------------------------------------------------------------------------- // Chart directions //--------------------------------------------------------------------------- @@ -178,7 +185,7 @@ function ChartView({settings, doc, updateDoc}) { const {start: selectStart, rotate: selectRotate} = mode2rotate(doc.ui.arc.mode) //--------------------------------------------------------------------------- - // Data selection + // Chart //--------------------------------------------------------------------------- return @@ -187,8 +194,8 @@ function ChartView({settings, doc, updateDoc}) { startAngle={selectStart + selectRotate * 1} endAngle={selectStart + selectRotate * (360 - 2)} innerData={tmplButtons[doc.ui.arc.template].data} - outerData={elemButtons[doc.ui.arc.elements].data(section)} - outerLabels={elemButtons[doc.ui.arc.elements].labels(section)} + outerData={data.map(e => e.data).flat()} + outerLabels={data.map(e => e.label)} /> } @@ -219,6 +226,12 @@ function ChartToolbar({settings}) { // Data generation for pie chart //----------------------------------------------------------------------------- +const elemButtons = { + act: {icon: "Acts"}, + chapter: {icon: "Chapters"}, + scene: {icon: "Scenes"}, +} + function elemLabel(elem) { const {words} = elem const name = elemName(elem) @@ -246,42 +259,43 @@ function elemData(elem) { ] } -function chapterLabels(section) { - return section.chapters.map(chapter => elemLabel(chapter)) -} +//----------------------------------------------------------------------------- +// Story data +//----------------------------------------------------------------------------- -function chapterData(section) { - return section.chapters.map(chapter => elemData(chapter)).flat() -} +function flatSection(section) { + return section.acts.map(flatAct).flat() -function sceneLabels(section) { - return section.chapters.map(chapter => ( - filterCtrlElems(chapter.children).map(scene => elemLabel(scene)) - )).flat() -} - -function sceneData(section) { - return section.chapters.map(chapter => ( - filterCtrlElems(chapter.children).map(scene => elemData(scene)).flat() - )).flat() -} + function flatAct(act) { + return [ + { + type: "act", + label: elemLabel(act), + data: elemData(act) + }, + ...filterCtrlElems(act.children).map(flatChapter).flat() + ] + } -//----------------------------------------------------------------------------- -// Story data -//----------------------------------------------------------------------------- + function flatChapter(chapter) { + return [ + { + type: "chapter", + label: elemLabel(chapter), + data: elemData(chapter) + }, + ...filterCtrlElems(chapter.children).map(flatScene) + ] + } -const elemButtons = { - scenes: { - icon: "Scenes", - labels: (section) => sceneLabels(section), - data: (section) => sceneData(section), - }, - chapters: { - icon: "Chapters", - labels: (section) => chapterLabels(section), - data: (section) => chapterData(section), - }, + function flatScene(scene) { + return { + type: "scene", + label: elemLabel(scene), + data: elemData(scene) + } + } } //----------------------------------------------------------------------------- diff --git a/src/gui/common/docIndex.js b/src/gui/common/docIndex.js index 40172dda..265ed713 100644 --- a/src/gui/common/docIndex.js +++ b/src/gui/common/docIndex.js @@ -22,11 +22,17 @@ import { import {FormatWords} from "./components"; import {elemAsText, elemName, filterCtrlElems} from "../../document"; -import {elemUnnumbered, wcCumulative} from "../../document/util"; +import {elemNumbered, wcCumulative} from "../../document/util"; //----------------------------------------------------------------------------- -export function DocIndex({name, style, activeID, section, wcFormat, include, setActive, unfold, current}) +function getCurrent(parents, include) { + if(!parents) return + const visible = parents.filter(e => include.includes(e.type)) + return visible[visible.length-1] +} + +export function DocIndex({name, style, activeID, section, wcFormat, include, setActive, unfold, parents}) { const refCurrent = useRef(null) @@ -34,6 +40,13 @@ export function DocIndex({name, style, activeID, section, wcFormat, include, set if(refCurrent.current) refCurrent.current.scrollIntoViewIfNeeded() }, [refCurrent.current]) + //--------------------------------------------------------------------------- + // Blocks -> current + //--------------------------------------------------------------------------- + + const current = getCurrent(parents, include) + //console.log(current) + //--------------------------------------------------------------------------- // Activation function //--------------------------------------------------------------------------- @@ -71,6 +84,12 @@ export function DocIndex({name, style, activeID, section, wcFormat, include, set ) //console.log(wcFormatFunction) + //--------------------------------------------------------------------------- + // Single unnamed act -> don't show + //--------------------------------------------------------------------------- + + const skipActName = (section.acts.length === 1 && !elemName(section.acts[0])) + //--------------------------------------------------------------------------- // Included items //--------------------------------------------------------------------------- @@ -82,58 +101,74 @@ export function DocIndex({name, style, activeID, section, wcFormat, include, set //--------------------------------------------------------------------------- return - + skipActName={skipActName} + /> + )} //return useDeferredValue(index) } //----------------------------------------------------------------------------- -const IndexHead = memo(({name, section, wcFormat}) => { - const wcFormatFunction = useCallback( - (!wcFormat || wcFormat === "off") - ? undefined - : (id, words) => , [wcFormat] - ) +class ActItem extends React.PureComponent { + + render() { + const {skipActName, elem, wcFormat, activeID, include, onActivate, unfold, current, refCurrent} = this.props - return -}) + const hasDropzone = (include.includes("chapter")) && (unfold || !elem.folded) + //const hasDropzone = (unfold || !elem.folded) + + return
+ {!skipActName && } + {hasDropzone && } +
+ } +} //----------------------------------------------------------------------------- class ChapterDropZone extends React.PureComponent { render() { - const {chapters, activeID} = this.props + const {chapters, id} = this.props if(!chapters) return null //console.log("Index update:", activeID) - return + return {this.DropZone.bind(this)} } @@ -141,13 +176,14 @@ class ChapterDropZone extends React.PureComponent { DropZone(provided, snapshot) { const {chapters, wcFormat, include, onActivate, unfold, current, refCurrent} = this.props const {innerRef, droppableProps, placeholder} = provided + const {isDraggingOver} = snapshot return
- {chapters.map((elem, index) => "}/> + {/**/} {wcFormat && wcFormat(id, words)} - {/* - - */} } @@ -342,6 +388,7 @@ class IndexItem extends React.PureComponent { function ScrollRef({current, id, refCurrent, children}) { if(current === id) { + //console.log("Match:", current, id) return
{children}
} return children diff --git a/src/gui/common/factory.js b/src/gui/common/factory.js index 1257373c..1519fb2c 100644 --- a/src/gui/common/factory.js +++ b/src/gui/common/factory.js @@ -190,13 +190,7 @@ export class MakeToggleGroup extends React.PureComponent { const {tooltip, icon} = buttons[choice] - if(tooltip) return - - {icon} - - - - return {icon} + return {icon} } } } diff --git a/src/gui/common/styles/TOC.css b/src/gui/common/styles/TOC.css index 27315c44..eb9e7959 100644 --- a/src/gui/common/styles/TOC.css +++ b/src/gui/common/styles/TOC.css @@ -31,7 +31,7 @@ background: white; border-right: 1pt solid lightgray; */ - counter-reset: chapter scene; + counter-reset: act chapter scene; } /* ---------------------------------------------------------------------------- */ @@ -46,7 +46,7 @@ /* ---------------------------------------------------------------------------- */ -.TOC .ChapterName { +.TOC .ActName { font-weight: bold; /* padding-top: 4pt !important; @@ -54,6 +54,44 @@ */ } +.TOC .ActName .Name { + text-transform: uppercase; +} + +.TOC .ActName.Numbered .Name::before { + counter-increment: act; + content: "ACT " counter(act, upper-roman) ": "; +/* + content: counter(chapter) ". "; +*/ +} + +.TOC div.ChapterDropZone { + min-height: 16pt; +} + +.TOC div.ChapterDropZone.DragOver { + margin-left: -4px; + margin-right: -4px; + border: 1px dashed grey; + padding: 3px; + /* + border-radius: 4px; + padding-top: 6px; + padding-bottom: 6px; + */ +} + +/* ---------------------------------------------------------------------------- */ + +.TOC .ChapterName { + /* + font-weight: bold; + padding-top: 4pt !important; + padding-bottom: 4pt !important; + */ +} + .TOC .ChapterName.Numbered .Name::before { counter-increment: chapter; content: counter(chapter) ". "; @@ -70,9 +108,9 @@ margin-left: -4px; margin-right: -4px; border: 1px dashed grey; - border-radius: 4px; padding: 3px; /* + border-radius: 4px; padding-top: 6px; padding-bottom: 6px; */ @@ -100,7 +138,7 @@ } .TOC .SceneName { - padding-left: 0.5cm !important; + padding-left: 1.00cm !important; /* padding-top: 4pt !important; padding-bottom: 4pt !important; diff --git a/src/gui/common/styles/sheet.css b/src/gui/common/styles/sheet.css index 5deff48f..a680466f 100644 --- a/src/gui/common/styles/sheet.css +++ b/src/gui/common/styles/sheet.css @@ -42,7 +42,7 @@ font-size: 12pt; line-height: 180%; font-family: 'Times New Roman', Times, serif; - counter-reset: chapter scene; + counter-reset: act chapter scene; background: white; @@ -110,7 +110,16 @@ margin-bottom: 0.5cm; } -/* For debugging purposes: make chapter & scene div's visible */ +/* ---------------------------------------------------------------------------- +// For debugging purposes: make chapter & scene div's visible +// ---------------------------------------------------------------------------- +*/ + +.Sheet div.act.withBorders { + border: 1pt solid lightblue; + padding: 4pt; + margin-bottom: 4pt; +} .Sheet div.chapter.withBorders { border: 1pt dashed gray; @@ -141,8 +150,8 @@ */ } -.Sheet div.folded::after { - margin-left: 6pt; +.Sheet div.folded::before { + margin-right: 6pt; content: "•••"; font-size: 10pt; color: grey; @@ -154,19 +163,19 @@ */ } -.Sheet div.folded h5, -.Sheet div.folded h6 { - display: inline; - /* - */ -} - .Sheet div.folded p, .Sheet div.folded div.emptyline, +.Sheet .act.folded div.chapter, .Sheet .chapter.folded div.scene { display: none } +.Sheet div.folded h4, +.Sheet div.folded h5, +.Sheet div.folded h6 { + display: inline; +} + .Sheet p.folded { white-space: nowrap; overflow: hidden; @@ -199,14 +208,6 @@ text-align: center; } -/* Chapter */ .Sheet h3 { - font-size: 12pt; -/* - font-weight: bold; -*/ - margin-bottom: 0.5cm; -} - /* Page break */ .Sheet hr { border: 0; border-top: 1px dashed LightSteelBlue; @@ -217,6 +218,20 @@ // ---------------------------------------------------------------------------- */ +/* Act */ .Sheet h4 { + font-family: Arial, Helvetica, sans-serif; + font-size: inherit; + color: #888; + text-transform: uppercase; + text-align: center; +} + +.Sheet h4.Numbered::before { + margin-right: 4pt; + counter-increment: act; + content: "Act " counter(act, upper-roman) ":"; +} + /* Chapter */ .Sheet h5 { font-family: Arial, Helvetica, sans-serif; font-size: inherit; @@ -233,7 +248,7 @@ } .Sheet h5.Numbered::before { - margin-right: 8pt; + margin-right: 4pt; counter-increment: chapter; content: counter(chapter) "."; } @@ -249,7 +264,7 @@ */ } -.Sheet h6.Numbered::before { +.Sheet h6::before { counter-increment: scene; margin-right: 8pt; content: "##"; diff --git a/src/gui/editor/editor.js b/src/gui/editor/editor.js index fa070ace..110e6b10 100644 --- a/src/gui/editor/editor.js +++ b/src/gui/editor/editor.js @@ -76,9 +76,16 @@ export function loadEditorSettings(settings) { if(!body) return {} const {words, indexed} = body.attributes + const fixed = (indexed ?? ["scene"]) + .split(",") + .filter(s => s !== "part") + .filter(s => s !== "chapter") + .filter(s => s !== "act") + .concat(["act", "chapter"]) + return { ...(words ? {words} : {}), - ...(indexed ? {indexed: indexed.split(",")} : {}) + ...(indexed ? {indexed: fixed} : {}) } } @@ -86,12 +93,12 @@ export function loadEditorSettings(settings) { active: "body", focusTo: {id: undefined}, body: { - indexed: ["chapter", "scene", "synopsis"], + indexed: ["act", "chapter", "scene", "synopsis"], words: "numbers", ...getBodySettings() }, notes: { - indexed: ["chapter", "scene", "synopsis"], + indexed: ["act", "chapter", "scene", "synopsis"], words: undefined, }, left: { @@ -151,6 +158,8 @@ export function SingleEditView({doc, updateDoc}) { // For development purposes: //--------------------------------------------------------------------------- + //console.log("Doc:", doc) + /* return @@ -170,21 +179,24 @@ export function SingleEditView({doc, updateDoc}) { const [track, setTrack] = useState({ marks: {}, - block: {}, node: undefined, + parents: [], }) const trackMarks = useCallback((editor) => { try { const marks = Editor.marks(editor) - const [node] = Editor.above(editor, {match: n => elemIsBlock(editor, n)}) - const [block] = Editor.above(editor, {match: n => elemIsBlock(editor, n) && (n.type === "scene" || n.type === "chapter")}) - - //console.log("Track:", marks, node, block) - - setTrack({block, node, marks,}) + const parents = Array + .from(Editor.levels(editor)) + .map(([n, p]) => n) + .filter(e => elemIsBlock(editor, e)); + const node = parents[parents.length - 1] + //console.log("Levels:", levels) + + //console.log("Track:", marks, node, parents) + setTrack({marks, node, parents}) } catch(e) { - //console.log("Track marks error.") + //console.log("Track error:", e) } }, [setTrack]) @@ -199,7 +211,7 @@ export function SingleEditView({doc, updateDoc}) { trackMarks(bodyeditor) if(isAstChange(bodyeditor)) { updateDoc(doc => { - doc.body.chapters = buffer; + doc.body.acts = buffer; doc.body.words = wcElem({type: "sect", children: buffer}) }) } @@ -209,7 +221,7 @@ export function SingleEditView({doc, updateDoc}) { trackMarks(noteeditor) if(isAstChange(noteeditor)) { updateDoc(doc => { - doc.notes.chapters = buffer + doc.notes.acts = buffer doc.notes.words = wcElem({type: "sect", children: buffer}) }) } @@ -286,12 +298,12 @@ export function SingleEditView({doc, updateDoc}) { track, body: { editor: bodyeditor, - buffer: doc.body.chapters, + buffer: doc.body.acts, onChange: updateBody, }, notes: { editor: noteeditor, - buffer: doc.notes.chapters, + buffer: doc.notes.acts, onChange: updateNotes, }, } @@ -376,6 +388,9 @@ export function SingleEditView({doc, updateDoc}) { console.log("onDragEnd:", result) + //console.log("DnD disabled") + //return + const {type, draggableId, source, destination} = result; if(!destination) return; @@ -399,6 +414,7 @@ export function SingleEditView({doc, updateDoc}) { } switch(type) { + case "chapter": case "scene": { const srcEditID = getSectIDByElemID(source.droppableId) const dstEditID = getSectIDByElemID(destination.droppableId) @@ -409,15 +425,6 @@ export function SingleEditView({doc, updateDoc}) { break; } - case "chapter": { - const srcEditID = source.droppableId - const dstEditID = destination.droppableId - const srcEdit = getEditorBySectID(srcEditID) - const dstEdit = getEditorBySectID(dstEditID) - - moveElem(srcEdit, draggableId, dstEditID, dstEdit, null, destination.index) - break; - } default: console.log("Unknown draggable type:", type, result) break; @@ -445,7 +452,7 @@ function LeftPanel({settings}) { wcFormat={doc.ui.editor.body.words} activeID="body" setActive={setActive} - current={track.block.id} + parents={track.parents} /> } @@ -517,7 +524,7 @@ function RightPanelContent({settings, selected}) { wcFormat={doc.ui.editor.notes.words} activeID="notes" setActive={setActive} - current={track.block.id} + parents={track.parents} /> case "wordtable": return case "chapter": return
case "scene": return
+ case "hact": return

case "hchapter": return

- case "hscene": return
+ case "hscene": return
case "comment": case "missing": @@ -214,6 +217,29 @@ onPaste={useCallback( )} */ +//***************************************************************************** +// +// Marks +// +//***************************************************************************** + +function isMarkActive(editor, format) { + const marks = Editor.marks(editor) + return marks ? marks[format] === true : false +} + +function setMark(editor, format, active) { + if (active) { + Editor.addMark(editor, format, true) + } else { + Editor.removeMark(editor, format) + } +} + +function toggleMark(editor, format) { + setMark(editor, format, !isMarkActive(editor, format)) +} + //***************************************************************************** // // Tool buttons @@ -258,20 +284,23 @@ class CharStyleButtons extends React.PureComponent { //----------------------------------------------------------------------------- -class BlockStyleSelect extends React.PureComponent { - - static choices = { - "p": {name: "Text", markup: "", shortcut: "Ctrl-Alt-0"}, - "hchapter": {name: "Chapter", markup: "#", shortcut: "Ctrl-Alt-1"}, - "hscene": {name: "Scene", markup: "##", shortcut: "Ctrl-Alt-2"}, - "synopsis": {name: "Synopsis", markup: ">>", shortcut: "Ctrl-Alt-S"}, - "comment": {name: "Comment", markup: "//", shortcut: "Ctrl-Alt-C"}, - "missing": {name: "Missing", markup: "!!", shortcut: "Ctrl-Alt-M"}, - "fill": {name: "Filler", markup: "++", shortcut: "Ctrl-Alt-F"}, - "tags": {name: "Tags", markup: "@", shortcut: ""}, - } +const nodeStyles = { + "p": {name: "Text", markup: "", shortcut: "Ctrl-Alt-0"}, + "hact": {name: "Act", markup: "**", shortcut: "Ctrl-Alt-1"}, + "hchapter": {name: "Chapter", markup: "#", shortcut: "Ctrl-Alt-2"}, + "hscene": {name: "Scene", markup: "##", shortcut: "Ctrl-Alt-3"}, + "synopsis": {name: "Synopsis", markup: ">>", shortcut: "Ctrl-Alt-S"}, + "comment": {name: "Comment", markup: "//", shortcut: "Ctrl-Alt-C"}, + "missing": {name: "Missing", markup: "!!", shortcut: "Ctrl-Alt-M"}, + "fill": {name: "Filler", markup: "++", shortcut: "Ctrl-Alt-F"}, + "tags": {name: "Tags", markup: "@", shortcut: ""}, +} + +//----------------------------------------------------------------------------- + +class NodeStyleSelect extends React.PureComponent { - static order = ["p", "hchapter", "hscene", "synopsis", "comment", "missing", "fill", "tags"] + static order = ["p", "hact", "hchapter", "hscene", "synopsis", "comment", "missing", "fill", "tags"] render() { const {type, setSelected} = this.props; @@ -279,7 +308,7 @@ class BlockStyleSelect extends React.PureComponent { //console.log("Block type:", type) - const choices = this.constructor.choices + const choices = nodeStyles const order = this.constructor.order const name = type in choices ? choices[type].name : "Text" @@ -346,7 +375,7 @@ export function EditButtons({editor, track}) { }, [editor]) return <> - + @@ -354,33 +383,23 @@ export function EditButtons({editor, track}) { //***************************************************************************** // -// Marks +// Custom hotkeys // //***************************************************************************** -function isMarkActive(editor, format) { - const marks = Editor.marks(editor) - return marks ? marks[format] === true : false -} +function toggleNumbering(editor, type) { + const [node, path] = Editor.above(editor, { + match: n => Editor.isBlock(editor, n), + }) -function setMark(editor, format, active) { - if (active) { - Editor.addMark(editor, format, true) - } else { - Editor.removeMark(editor, format) + if(node.type === type) { + const {numbered} = node + Transforms.setNodes(editor, {numbered: !numbered}) + return true; } + return false } -function toggleMark(editor, format) { - setMark(editor, format, !isMarkActive(editor, format)) -} - -//***************************************************************************** -// -// Custom hotkeys -// -//***************************************************************************** - function onKeyDown(editor, event) { //--------------------------------------------------------------------------- @@ -464,7 +483,7 @@ function onKeyDown(editor, event) { } //--------------------------------------------------------------------------- - // Block styles + // Node styles //--------------------------------------------------------------------------- if(IsKey.CtrlAlt0(event)) { @@ -475,20 +494,20 @@ function onKeyDown(editor, event) { if(IsKey.CtrlAlt1(event)) { event.preventDefault() - const [node, path] = Editor.above(editor, { - match: n => Editor.isBlock(editor, n), - }) - //console.log(node) - if(node.type === "hchapter") { - const {unnumbered} = node - Transforms.setNodes(editor, {unnumbered: !unnumbered}) - return; - } - Transforms.setNodes(editor, {type: "hchapter"}) + //if(toggleNumbering(editor, "hact")) return + Transforms.setNodes(editor, {type: "hact", numbered: undefined}) return ; } if(IsKey.CtrlAlt2(event)) { + event.preventDefault() + //console.log(node) + if(toggleNumbering(editor, "hchapter")) return + Transforms.setNodes(editor, {type: "hchapter", numbered: true}) + return ; + } + + if(IsKey.CtrlAlt3(event)) { event.preventDefault() Transforms.setNodes(editor, {type: "hscene"}) return ; @@ -644,6 +663,7 @@ function withTextPaste(editor) { //----------------------------------------------------------------------------- const blockstyles = { + "hact": { next: "p", bk: true, }, "hchapter": { next: "p", bk: true, }, "hscene": { next: "p", bk: true, }, "synopsis": { next: "p", reset: true, bk: true, }, @@ -656,8 +676,9 @@ const blockstyles = { // TODO: Generate this table const MARKUP = { - "# " : {type: "hchapter", unnumbered: undefined}, - "#! ": {type: "hchapter", unnumbered: true}, + "** ": {type: "hact", numbered: undefined}, + "# " : {type: "hchapter", numbered: true}, + "#! ": {type: "hchapter", numbered: undefined}, "## ": {type: "hscene"}, '>> ': {type: "synopsis"}, '// ': {type: 'comment'}, @@ -696,9 +717,7 @@ function withMarkup(editor) { if(key in MARKUP) { Transforms.select(editor, range) Transforms.delete(editor) - //const {type, unnumbered} = MARKUP[key] Transforms.setNodes(editor, MARKUP[key]) - //if(type === "hchapter") setUnnumbering(editor, path, unnumbered) return } @@ -790,6 +809,13 @@ function withIDs(editor) { const { normalizeNode } = editor; + const indexable = new Set([ + "act", "chapter", "scene", + "synopsis", "comment", + "missing", "fill", + "tags" + ]) + editor.normalizeNode = (entry)=> { const [node, path] = entry @@ -800,7 +826,10 @@ function withIDs(editor) { const blocks = Editor.nodes(editor, { at: [], - match: (node, path) => !Editor.isEditor(node) && Element.isElement(node), + match: (node, path) => ( + Element.isElement(node) + && indexable.has(node.type) + ), }) //console.log(Array.from(blocks)) @@ -910,18 +939,18 @@ function withProtectFolds(editor) { editor.deleteBackward = (options) => { unfoldSelection() - deleteBackward(options) + return deleteBackward(options) } editor.deleteForward = (options) => { unfoldSelection() - deleteForward(options) + return deleteForward(options) } editor.insertText = (text, options) => { unfoldSelection() //console.log("insertText", text, options) - insertText(text, options) + return insertText(text, options) } /* @@ -977,57 +1006,87 @@ function withProtectFolds(editor) { // //----------------------------------------------------------------------------- + function withFixNesting(editor) { const { normalizeNode } = editor; + const blockTypes = { + "act": {header: "hact", level: 1, contains: "chapter", }, + "chapter": {header: "hchapter", level: 2, wrap: "act" , contains: "scene"}, + "scene": {header: "hscene", level: 3, wrap: "chapter", }, + } + + const blockHeaders = { + "hact": "act", + "hchapter": "chapter", + "hscene": "scene", + } + editor.normalizeNode = entry => { const [node, path] = entry //console.log("Fix:", path, node) if(Text.isText(node)) return normalizeNode(entry) - if(Editor.isEditor(node)) return normalizeNode(entry) - - switch(node.type) { - // Paragraph styles come first - case "hchapter": - if(!checkParent(node, path, "chapter")) return - if(!checkIsFirst(node, path, "chapter")) return - break; - case "hscene": - if(!checkParent(node, path, "scene")) return - if(!checkIsFirst(node, path, "scene")) return; - break; - default: - if(!checkParent(node, path, "scene")) return - break; - - // Block styles come next - case "chapter": { - if(path.length > 1) { - Transforms.liftNodes(editor, {at: path}) - return; - } - if(!checkBlockHeader(node, path, "hchapter")) return - break; + if(Editor.isEditor(node)) { + if(!mergeHeadlessChilds(node, path, "act", "hact")) return + return normalizeNode(entry) + } + + //console.log("Fix nesting:", node, path) + + // Block types + if(node.type in blockTypes) { + //console.log("Fix nesting: Block:", node.type) + + const blockType = blockTypes[node.type] + + if(!node.children.length) { + Transforms.removeNodes(editor, {at: path}) + return; } - case "scene": { - if(path.length < 2) { - Transforms.wrapNodes(editor, {type: "chapter"}, {at: path}) - return; - } else if(path.length > 2) { - Transforms.liftNodes(editor, {at: path}) - return - } - if(!checkBlockHeader(node, path, "hscene")) return - const match = Editor.next(editor, {at: path}) - if(!match) break; - if(!checkBlockHeader(match[0], match[1], "hscene")) return - break + + if(path.length > blockType.level) { + Transforms.liftNodes(editor, {at: path}) + return; + } + if(path.length < blockType.level) { + Transforms.wrapNodes(editor, {type: blockType.wrap}, {at: path}) + return; + } + + + if(blockType.contains) + { + const childType = blockTypes[blockType.contains] + if(!mergeHeadlessChilds(node, path, blockType.contains, childType.header)) return; } + + return normalizeNode(entry) } - //return + + /* + if(!checkBlockHeader(node, path, "hscene")) return + const match = Editor.next(editor, {at: path}) + if(!match) break; + if(!checkBlockHeader(match[0], match[1], "hscene")) return + break + } + */ + + // Block headers + if(node.type in blockHeaders) { + const blockType = blockHeaders[node.type] + + if(!checkParent(node, path, blockType)) return + if(!checkIsFirst(node, path, blockType)) return + + return normalizeNode(entry) + } + + // Paragraphs styles + if(!checkParent(node, path, "scene")) return return normalizeNode(entry) } @@ -1047,6 +1106,7 @@ function withFixNesting(editor) { //console.log("FixNesting: Wrapping", path, node, type) Transforms.wrapNodes(editor, {type}, {at: path}) + //console.log("Node:", node.type, "Parent:", parent.type, "->", type) return false } @@ -1070,30 +1130,32 @@ function withFixNesting(editor) { } //--------------------------------------------------------------------------- - // Ensure, that blocks have correct header element + // Merge childs without header //--------------------------------------------------------------------------- - function checkBlockHeader(block, path, type) { + function mergeHeadlessChilds(block, path, type, header) { - if(!block.children.length) { - Transforms.removeNodes(editor, {at: path}) - return false; - } + for(const child of Node.children(editor, path)) { + const [node, path] = child + if(node.type !== type) continue - const hdrtype = block.children[0].type + // Does the block have correct header type? + const childhdr = node.children[0].type + if(childhdr === header) continue - // Does the block have correct header type? - if(hdrtype === type) return true + const prev = Editor.previous(editor, {at: path}) - const prev = Editor.previous(editor, {at: path}) + //console.log("Headless:", block, "Previous:", prev) - // Can we merge headingless block? - if(prev && prev[0].type === block.type) { - doFold(editor, prev[0], prev[1], false) - doFold(editor, block, path, false) - Transforms.mergeNodes(editor, {at: path}) + // Can we merge headingless block? + if(prev && prev[0].type === type) { + //console.log("Merging") + doFold(editor, prev[0], prev[1], false) + doFold(editor, block, path, false) + Transforms.mergeNodes(editor, {at: path}) - return false + return false + } } // Otherwise the block is fine as it is diff --git a/src/gui/editor/slateHelpers.js b/src/gui/editor/slateHelpers.js index 4c057b94..54491d72 100644 --- a/src/gui/editor/slateHelpers.js +++ b/src/gui/editor/slateHelpers.js @@ -15,10 +15,9 @@ import { } from 'slate' import { ReactEditor } from 'slate-react' -import { sleep } from '../../util'; import { nanoid } from 'nanoid'; import { appBeep } from '../../system/host'; -import {elemTags} from '../../document/util'; +import {elemHeading, elemTags} from '../../document/util'; //----------------------------------------------------------------------------- // Search pattern @@ -46,7 +45,7 @@ export function searchPattern(text, opts = "gi") { //----------------------------------------------------------------------------- export function elemIsBlock(editor, elem) { - return elem && !Editor.isEditor(elem) && Editor.isBlock(editor, elem); + return elem && !Editor.isEditor(elem) && Element.isElement(elem); } function elemIsType(editor, elem, type) { @@ -120,6 +119,7 @@ export function elemsByRange(editor, anchor, focus) { // Drag'n'drop po and push export function dndElemPop(editor, id) { + const match = elemByID(editor, id) if(!match) return @@ -143,7 +143,7 @@ export function dndElemPop(editor, id) { } export function dndElemPushTo(editor, block, id, index) { - //console.log("Push", id, index) + //console.log("Push", block, id, index) if(!block) return @@ -152,30 +152,42 @@ export function dndElemPushTo(editor, block, id, index) { return elemByID(editor, id) } - const [container, cpath] = getContainer() + const [container, pcontainer] = getContainer() + + //console.log("Container:", container) + + //--------------------------------------------------------------------------- + // Check if container has head element. If so, add +1 to index + //--------------------------------------------------------------------------- function getChildIndex(container) { - const {type, children} = container - if(type === "chapter") { - if(children.length && children[0].type === "hchapter") { - return index+1 - } - } + if(elemHeading(container)) return index+1 return index } const childindex = getChildIndex(container) - const childpath = [...cpath, childindex] + const childpath = [...pcontainer, childindex] + + //--------------------------------------------------------------------------- + // Check that elem at drop point has header (prevent merge) + //--------------------------------------------------------------------------- + + const blockTypes = { + "act": {header: "hact", level: 1, contains: "chapter", }, + "chapter": {header: "hchapter", level: 2, wrap: "act" , contains: "scene"}, + "scene": {header: "hscene", level: 3, wrap: "chapter", }, + } if(container.children.length > childindex) { const node = container.children[childindex] - const htype = (node.type === "chapter") ? "hchapter" : "hscene" + const htype = blockTypes[node.type].header if(!node.children.length || node.children[0].type !== htype) { Transforms.insertNodes(editor, { type: htype, id: nanoid(), + numbered: true, children: [{text: ""}] }, {at: [...childpath, 0]} @@ -186,6 +198,7 @@ export function dndElemPushTo(editor, block, id, index) { //console.log("Index at:", [...ppath, index]) //console.log("Insert at:", childpath) Transforms.insertNodes(editor, block, {at: childpath}) + } //----------------------------------------------------------------------------- @@ -283,7 +296,7 @@ export function toggleFold(editor) { //console.log("Toggle fold", path, node) //const foldable = ["chapter", "scene", "synopsis", "comment", "missing"] - const foldable = ["chapter", "scene"] + const foldable = ["act", "chapter", "scene"] const [node, path] = Editor.above(editor, { at: anchor, @@ -314,40 +327,53 @@ export function foldByTags(editor, tags) { const tagset = new Set(tags) var folders = [] - // Go through chapters - for(const chapter of Node.children(editor, [])) - { - const [node, path] = chapter + // Go through acts, chapters and scenes + for(const act of Node.children(editor, [])) { + const [node, path] = act + + var acttags = new Set() + + for(const chapter of Node.children(editor, path)) + { + const [node, path] = chapter + + if(node.type !== "chapter") continue - var chaptertags = new Set() + var chaptertags = new Set() - // Go through scenes - for(const scene of Node.children(editor, path)) { - const [node, path] = scene - if(node.type !== "scene") continue + // Go through scenes + for(const scene of Node.children(editor, path)) { + const [node, path] = scene + if(node.type !== "scene") continue - const scenetags = new Set() + const scenetags = new Set() - // Go through blocks and get tags - for(const elem of Node.children(editor, path)) { - const [node, path] = elem + // Go through blocks and get tags + for(const elem of Node.children(editor, path)) { + const [node, path] = elem - for(const key of elemTags(node)) { - scenetags.add(key) + for(const key of elemTags(node)) { + scenetags.add(key) + } } + + const hastags = tagset.intersection(scenetags).size > 0 + folders.push({node, path, folded: !hastags}) + //console.log("Scene:", path, node.type, hastags, scenetags); + + chaptertags = chaptertags.union(scenetags) } - const hastags = tagset.intersection(scenetags).size > 0 + const hastags = tagset.intersection(chaptertags).size > 0 folders.push({node, path, folded: !hastags}) - //console.log("Scene:", path, node.type, hastags, scenetags); - chaptertags = chaptertags.union(scenetags) + acttags = acttags.union(chaptertags) + + //console.log("Chapter:", path, node.type, hastags, chaptertags); } - const hastags = tagset.intersection(chaptertags).size > 0 + const hastags = tagset.intersection(acttags).size > 0 folders.push({node, path, folded: !hastags}) - - //console.log("Chapter:", path, node.type, hastags, chaptertags); } Editor.withoutNormalizing(editor, () => { diff --git a/src/gui/export/export.js b/src/gui/export/export.js index 59686aa4..7a762afc 100644 --- a/src/gui/export/export.js +++ b/src/gui/export/export.js @@ -66,6 +66,7 @@ export function loadExportSettings(settings) { return { format: "rtf1", type: "short", + acts: "none", chapters: "numbered", scenes: "none", ...(settings?.attributes ?? {}) @@ -73,9 +74,10 @@ export function loadExportSettings(settings) { } export function saveExportSettings(settings) { - const {type, chapters, scenes} = settings + const {type, acts, chapters, scenes} = settings return {type: "export", attributes: { type, + acts, chapters, scenes, }} @@ -104,6 +106,7 @@ export function Export({ doc, updateDoc }) { //----------------------------------------------------------------------------- function updateDocStoryType(updateDoc, value) { updateDoc(doc => {doc.exports.type = value})} +function updateDocActElem(updateDoc, value) { updateDoc(doc => {doc.exports.acts = value})} function updateDocChapterElem(updateDoc, value) { updateDoc(doc => {doc.exports.chapters = value})} function updateDocSceneElem(updateDoc, value) { updateDoc(doc => {doc.exports.scenes = value})} @@ -136,6 +139,12 @@ function ExportSettings({ style, doc, updateDoc, format, setFormat }) { Long Story + updateDocActElem(updateDoc, e.target.value)}> + Named + Separated + None + + updateDocChapterElem(updateDoc, e.target.value)}> Numbered Named @@ -145,8 +154,8 @@ function ExportSettings({ style, doc, updateDoc, format, setFormat }) { updateDocSceneElem(updateDoc, e.target.value)}> - None Separated + None @@ -170,17 +179,33 @@ async function exportToFile(doc, filesuffix, content) { } //----------------------------------------------------------------------------- -// Export index +// Export index / TODO: Generate from exported data //----------------------------------------------------------------------------- function ExportIndex({ style, doc, updateDoc }) { - const { chapters } = doc.body + const { acts } = doc.body return - {filterCtrlElems(chapters).map(chapter => )} + {filterCtrlElems(acts).map(act => )} } +function ActItem({act, doc, updateDoc}) { + const { id, children } = act + const name = elemName(act) + return <> +
window.location.href = `#${id}`} + onDoubleClick={ev => setFocusTo(updateDoc, "body", id)} + style={{ cursor: "pointer" }} + > + ACT: {name} +
+ {filterCtrlElems(children).map(chapter => )} + +} + function ChapterItem({ chapter, doc, updateDoc }) { const { id, children } = chapter const name = elemName(chapter) diff --git a/src/gui/export/formatDoc.js b/src/gui/export/formatDoc.js index 337a9f65..0af29302 100644 --- a/src/gui/export/formatDoc.js +++ b/src/gui/export/formatDoc.js @@ -12,6 +12,25 @@ import { splitByTrailingElem } from "../../util"; // Settings //***************************************************************************** +function getActOptions(acts, pgbreak) { + switch(acts) { + case "named": return { + numbered: { pgbreak, name: true}, + unnumbered: { pgbreak, name: true } + } + case "separated": return { + separator: "* * *", + numbered: { skip: true }, + unnumbered: { skip: true } + } + } + return { + numbered: {skip: true}, + unnumbered: {skip: true}, + } +} + + function getChapterOptions(chapters, pgbreak) { switch(chapters) { case "numbered": return { @@ -57,31 +76,33 @@ export function FormatBody(format, story) { const options = { long: exports.type === "long", + act: getActOptions(exports.acts, pgbreak), chapter: getChapterOptions(exports.chapters, pgbreak), scene: getSceneOptions(exports.scenes) } + var actnum = 0 var chapternum = 0 var scenenum = 0 - /* - const chapters = { - element: exports.chapterelem, - type: exports.chaptertype, - } - const pgbreak = exports.type === "long" - */ - return format.file( mawe.info(head), - FormatBody(body.chapters), + FormatBody(body.acts), options ) - function FormatBody(chapters) { - const content = chapters.map(FormatChapter).filter(p => p) + function FormatBody(acts) { + const content = acts.map(FormatAct).filter(p => p) - return format.body(content, options.chapter) + return format.body(content, options.act) + } + + function FormatAct(act) { + return format.chapter( + FormatActHead(act), + act.children.filter(e => e.type === "chapter").map(FormatChapter).filter(s => s), + options.chapter + ) } function FormatChapter(chapter) { @@ -92,17 +113,33 @@ export function FormatBody(format, story) { ) } + function FormatActHead(act) { + + const {id} = act + const head = elemHeading(act) + + const numbered = head?.numbered + + if(numbered) { + actnum = actnum + 1 + return format.hact(id, actnum, elemAsText(head), options.act.numbered) + } else { + return format.hact(id, undefined, elemAsText(head), options.act.unnumbered) + } + } + function FormatChapterHead(chapter) { const {id} = chapter const head = elemHeading(chapter) - const {unnumbered} = head - if(unnumbered) { - return format.hchapter(id, undefined, elemAsText(head), options.chapter.unnumbered) - } else { + const numbered = head?.numbered + + if(numbered) { chapternum = chapternum + 1 return format.hchapter(id, chapternum, elemAsText(head), options.chapter.numbered) + } else { + return format.hchapter(id, undefined, elemAsText(head), options.chapter.unnumbered) } } diff --git a/src/gui/export/formatHTML.js b/src/gui/export/formatHTML.js index 31c4b2e7..05f36a59 100644 --- a/src/gui/export/formatHTML.js +++ b/src/gui/export/formatHTML.js @@ -49,6 +49,19 @@ ${content} // Headings //--------------------------------------------------------------------------- + hact: (id, number, name, options) => { + if(options.skip) return `
` + + const pgbreak = options.pgbreak ? "
\n" : "" + const numbering = options.number ? [escape(`${options.prefix ?? ""}${number}`)] : [] + const title = options.name ? [escape(name)] : [] + const head = [ ...numbering, ...title].join(". ") + + if(!head) return "" + + return `${pgbreak}

${head}

` + }, + hchapter: (id, number, name, options) => { if(options.skip) return `
` @@ -57,6 +70,8 @@ ${content} const title = options.name ? [escape(name)] : [] const head = [ ...numbering, ...title].join(". ") + if(!head) return "" + return `${pgbreak}

${head}

` }, diff --git a/src/gui/export/formatRTF.js b/src/gui/export/formatRTF.js index d4b6781c..553bc26a 100644 --- a/src/gui/export/formatRTF.js +++ b/src/gui/export/formatRTF.js @@ -60,7 +60,7 @@ ${fonts} ${colors} {\\info {\\title ${escape(title)}} -{\\author ${escape(author)}} +${author ? `{\\author ${escape(author)}}` : ""} } \\deflang${langcode} ${singleA4} @@ -74,7 +74,7 @@ ${escape(headinfo)}\\tab ${pgnum} / ${pgtot} \\lang${langcode} \\sl440 -{\\sa220\\qc ${escape(author)}\\par} +${author ? `{\\sa220\\qc ${escape(author)}\\par}` : ""} {\\sa440\\qc\\b\\fs34 ${escape(title)}\\par} ${subtitle ? "{\\sa440\\qc\\b\\fs28" + escape(subtitle) + "\\par}" : ""} @@ -106,6 +106,19 @@ ${content} // Headings //--------------------------------------------------------------------------- + hact: (id, number, name, options) => { + if(options.skip) return "" + + const pgbreak = options.pgbreak ? "\\pagebb" : "" + const numbering = options.number ? [escape(`${options.prefix ?? ""}${number}`)] : [] + const title = options.name ? [escape(name)] : [] + const head = [ ...numbering, ...title].join(". ") + + if(!head) return "" + + return `{${pgbreak}\\sb1000\\qc\\b\\fs32 ${head}\\par}\n` + }, + hchapter: (id, number, name, options) => { if(options.skip) return "" @@ -114,6 +127,8 @@ ${content} const title = options.name ? [escape(name)] : [] const head = [ ...numbering, ...title].join(". ") + if(!head) return "" + return `{${pgbreak}\\b\\fs28 ${head}\\par}\n` }, diff --git a/src/gui/export/formatTEX.js b/src/gui/export/formatTEX.js index 3f2c4bd4..c90aa05c 100644 --- a/src/gui/export/formatTEX.js +++ b/src/gui/export/formatTEX.js @@ -56,6 +56,13 @@ function renewCommands(options, sides) { ${pgbreak ? newpage : "\\vskip 48pt"} } +\\renewcommand\\part[1] { + \\if@titlepage${newpage}\\fi + \\null\\vskip 2cm + \\begin{center}{\\Huge #1}\\end{center} +} + + \\newcommand\\innertitle{{\\center{\\Large\\@title\\vskip 48pt}}} \\newcommand{\\RNum}[1]{\\uppercase\\expandafter{\\romannumeral #1\\relax}} @@ -183,11 +190,24 @@ ${backmatter} // Headings //--------------------------------------------------------------------------- + hact: (id, number, name, options) => { + if(options.skip) return "" + + const chnum = options.number ? escape(`${options.prefix ?? ""}${number}`) : "" + const title = options.name ? escape(name) : "" + + if(!chnum && !title) return "" + + return `\n\n\\part{${title}}\n\n` + }, + hchapter: (id, number, name, options) => { if(options.skip) return "" - const chnum = options.number ? [escape(`${options.prefix ?? ""}${number}`)] : [] - const title = options.name ? [escape(name)] : [] + const chnum = options.number ? escape(`${options.prefix ?? ""}${number}`) : "" + const title = options.name ? escape(name) : "" + + if(!chnum && !title) return "" return `\n\n\\chapter{${chnum}}{${title}}\n\n` }, diff --git a/src/gui/export/formatTXT.js b/src/gui/export/formatTXT.js index ddb33627..fe52b61a 100644 --- a/src/gui/export/formatTXT.js +++ b/src/gui/export/formatTXT.js @@ -40,6 +40,18 @@ ${content} split: (paragraphs) => paragraphs.join("\n "), + hact: (id, number, name, options) => { + if(options.skip) return "" + + const numbering = options.number ? [escape(`${options.prefix ?? ""}${number}`)] : [] + const title = options.name ? [escape(name)] : [] + const head = [ ...numbering, ...title].join(". ") + + if(!head) return "" + + return `${head}\n\n` + }, + hchapter: (id, number, name, options) => { if(options.skip) return "" @@ -47,6 +59,8 @@ ${content} const title = options.name ? [escape(name)] : [] const head = [ ...numbering, ...title].join(". ") + if(!head) return "" + return `${head}\n\n` }, @@ -106,6 +120,18 @@ ${content} // Headings //--------------------------------------------------------------------------- + hact: (id, number, name, options) => { + if(options.skip) return "" + + const numbering = options.number ? [escape(`${options.prefix ?? ""}${number}`)] : [] + const title = options.name ? [escape(name)] : [] + const head = [ ...numbering, ...title].join(". ") + + if(!head) return "" + + return `# ${head}\n\n` + }, + hchapter: (id, number, name, options) => { if(options.skip) return "" @@ -113,6 +139,8 @@ ${content} const title = options.name ? [escape(name)] : [] const head = [ ...numbering, ...title].join(". ") + if(!head) return "" + return `## ${head}\n\n` }, @@ -124,8 +152,8 @@ ${content} "missing": (p,text) => `!! ${text}`, "p": (p, text) => `${text}`, - "b": (text) => `[b]${text}[/b]`, - "i": (text) => `[i]${text}[/i]`, + "b": (text) => `**${text}**`, + "i": (text) => `_${text}_`, "text": (text) => text, } diff --git a/src/gui/import/import.js b/src/gui/import/import.js index 89e8be1a..6e19249f 100644 --- a/src/gui/import/import.js +++ b/src/gui/import/import.js @@ -108,9 +108,7 @@ function ImportBar({format, setFormat, imported, updateDoc, buffer, setBuffer}) const story = maweFromTree({ elements: [{ type: "element", name: "story", - attributes: { - format: "mawe" - }, + attributes: {format: "mawe", version: "4"}, elements: [ { type: "element", name: "body", diff --git a/src/gui/import/importText.js b/src/gui/import/importText.js index 2e22fca8..7bc15dee 100644 --- a/src/gui/import/importText.js +++ b/src/gui/import/importText.js @@ -29,6 +29,7 @@ export class ImportText extends React.PureComponent { super(props); this.state = { linebreak: "double", + actprefix: "", chapterprefix: "# ", sceneprefix: "## ", }; @@ -38,6 +39,10 @@ export class ImportText extends React.PureComponent { this.setState({linebreak}) } + setActPrefix(actprefix) { + this.setState({actprefix}) + } + setChapterPrefix(chapterprefix) { this.setState({chapterprefix}) } @@ -57,6 +62,7 @@ export class ImportText extends React.PureComponent { Double Single + this.setActPrefix(e.target.value)}/> this.setChapterPrefix(e.target.value)}/> this.setScenePrefix(e.target.value)}/> @@ -85,29 +91,55 @@ function importText(content, settings) { if(!content) return undefined const linebreak = getLinebreak(settings.linebreak) - const {chapterprefix, sceneprefix} = settings + const {actprefix, chapterprefix, sceneprefix} = settings + + function isActBreak(line) { + //console.log("Act prefix:", actprefix) + if(!actprefix.length) return false + if(!line) return false + return line.toLowerCase().startsWith(actprefix.toLowerCase()) + } function isChapterBreak(line) { if(!chapterprefix) return false - return line.startsWith(chapterprefix) + if(!line) return false + return line.toLowerCase().startsWith(chapterprefix.toLowerCase()) } function isSceneBreak(line) { if(!sceneprefix) return false - return line.startsWith(sceneprefix) + if(!line) return false + return line.toLowerCase().startsWith(sceneprefix.toLowerCase()) } const lines = text2lines(content, linebreak) - const chapters = splitByLeadingElem(lines, isChapterBreak) + const acts = splitByLeadingElem(lines, isActBreak).filter(e => e.length) - const elements = chapters.map(makeChapter) + const elements = acts.map(makeAct) - //console.log("Elements:", elements) + console.log("Elements:", elements) return elements //--------------------------------------------------------------------------- + function makeAct(lines) { + const {first, rest} = getContent(lines) + //console.log(first, rest) + const chapters = splitByLeadingElem(rest, isChapterBreak).filter(e => e.length) + return { + type: "element", name: "act", id: nanoid(), + attributes: { name: first }, + elements: chapters.map(makeChapter) + } + + function getContent(lines) { + const [first, ...rest] = lines + if(isActBreak(first)) return {first, rest} + return {first: "###", rest: [first].concat(rest)} + } + } + function makeChapter(lines) { const {first, rest} = getContent(lines) //console.log(first, rest) @@ -121,7 +153,7 @@ function importText(content, settings) { function getContent(lines) { const [first, ...rest] = lines if(isChapterBreak(first)) return {first, rest} - return {rest: [first].concat(rest)} + return {first: "###", rest: [first].concat(rest)} } } @@ -137,7 +169,7 @@ function importText(content, settings) { function getContent(lines) { const [first, ...rest] = lines if(isSceneBreak(first)) return {first, rest} - return {rest: [first].concat(rest)} + return {first: "* * *", rest: [first].concat(rest)} } } diff --git a/src/gui/import/preview.js b/src/gui/import/preview.js index 9a3aa526..6dbf85ea 100644 --- a/src/gui/import/preview.js +++ b/src/gui/import/preview.js @@ -23,13 +23,21 @@ export class Preview extends React.PureComponent { style={{borderRight: "1px solid lightgray", borderLeft: "1px solid lightgray"}} >
- {imported.map(PreviewChapter)} + {imported.map(PreviewAct)}
} } +function PreviewAct(act) { + return
+

{act.attributes.name}

+ {act.elements.map(PreviewChapter)} +
+} + + function PreviewChapter(chapter) { return
{chapter.attributes.name}
@@ -54,9 +62,16 @@ function PreviewParagraph(p) { function ImportIndex({imported}) { return
- {imported.map(chapterIndex)} + {imported.map(actIndex)}
+ function actIndex(act) { + return
+

{act.attributes.name}

+ {act.elements.map(chapterIndex)} +
+ } + function chapterIndex(chapter) { return

{chapter.attributes.name}