markupeditor 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Steven G. Harris
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,168 @@
1
+ <p align="center">
2
+ <img alt="The MarkupEditor logo" src="https://github.com/user-attachments/assets/c67b6aa0-2576-4a0b-81d0-229ee501b59d" width="96px" height="96px" >
3
+ </p>
4
+
5
+ # MarkupEditor
6
+
7
+ The MarkupEditor consists of a web component and API for WYSIWYG HTML editing. The "source of truth" for the MarkupEditor is standard HTML,
8
+ styled using standard CSS. The MarkupEditor comes with a toolbar and hot key bindings to access its editing functionality. The toolbar
9
+ visibility and contents, as well as the key bindings, are configurable and extensible.
10
+
11
+ ![Hello, MarkupEditor!](resources/hellomarkupeditor.png)
12
+
13
+ The web component makes embedding WYSIWYG editing as simple as:
14
+
15
+ ```
16
+ <markup-editor><h1>Hello, MarkupEditor!</h1></markup-editor>
17
+ <script src="markup-editor.js" type="module"></script>
18
+ ```
19
+
20
+ ### Why
21
+
22
+ You or the consumers of your application need to edit more than just plain text. You want to be able to format and organize a document with
23
+ headers, lists, links, images, and perhaps tables, but you do not want to do that using Markdown or require that the consumers of your app
24
+ know anything about Markdown or HTML. The MarkupEditor is the solution. It can be used in any environment that supports a web view, from a
25
+ browser to a mobile or desktop app.
26
+
27
+ Like Markdown, the MarkupEditor keeps the focus on what you're writing, with a minimum of distractions. Like Markdown, it supports just enough
28
+ functionality to help you organize and format your writing to get your points across effectively. Unlike Markdown, the MarkupEditor's WYSIWYG
29
+ approach means you always see what you're writing presented properly as you write it, instead of dealing with the distractions of composing
30
+ text with embedded notations and the uncertainty of how that text is later translated to HTML and presented.
31
+
32
+ ## Features
33
+
34
+ The MarkupEditor's standard editing features are generally limited to what
35
+ [Github Flavored Markdown](https://github.github.com/gfm/) supports. It does, however, have support
36
+ for some functionality beyond that baseline. As a WYSIWYG editor, it supports the kind of functionality that
37
+ even non-developers expect when they edit a document, like image resizing and search.
38
+
39
+ * Customizable and extensible toolbar providing access to all editing features, auto-sized to width.
40
+ * Customizable key mappings for hot-key access to editing features.
41
+ * Customizable ordering of toolbar contents.
42
+ * Customizable icons for toolbar.
43
+ * Paragraph styles corresponding to P, H1-H6, and `CODE`.
44
+ * Bold, italic, underline, strikethrough, subscript, superscript, and code text formatting.
45
+ * Insert and edit links, images (local and https src), and tables.
46
+ * Bulleted and numbered lists.
47
+ * Indent/outdent.
48
+ * Comprehensive undo/redo.
49
+ * Search.
50
+ * Image resizing using gestures.
51
+ * Table editing: visually select table size, add/remove row/column/header, border options.
52
+
53
+ The API provides hooks for callbacks as your document changes, as well as access to the edited HTML
54
+ so that you can save contents in a way that makes sense in your own context.
55
+
56
+ The web component and customization capabilities allow the MarkupEditor to be embedded in different
57
+ environments, such as an Electron-based [desktop editing app](https://github.com/stevengharris/markupeditor-desktop),
58
+ a [Swift library for WYSIWYG editing](https://github.com/stevengharris/MarkupEditor), and
59
+ a [VSCode extension](https://github.com/stevengharris/markupeditor-vs).
60
+
61
+ ## Try
62
+
63
+ You can try the MarkupEditor out right from the [project web site](https://stevengharris.github.io/markupeditor-base/). The web site has all
64
+ the information you need to use the MarkupEditor in your application.
65
+
66
+ ## Install
67
+
68
+ Clone the repository.
69
+
70
+ ```
71
+ git clone https://github.com/stevengharris/markupeditor-base.git
72
+ ```
73
+
74
+ You need node/npm installed. Install the dependencies.
75
+
76
+ ```
77
+ $ cd markupeditor-base/
78
+ $ npm install
79
+
80
+ added 265 packages, and audited 266 packages in 9s
81
+
82
+ 70 packages are looking for funding
83
+ run `npm fund` for details
84
+
85
+ found 0 vulnerabilities
86
+ $
87
+ ```
88
+
89
+ Build the project.
90
+
91
+ ```
92
+ $ npm run build
93
+
94
+ > markupeditor@0.9.0 build
95
+ > rollup -c
96
+
97
+
98
+ src/main.js → dist/markup-editor.js...
99
+ created dist/markup-editor.js in 300ms
100
+ ```
101
+
102
+ The installation identifies `muedit` in the package.json `bin` property. You can open the editor on a doc using node.js with
103
+ the `muedit` command, providing a filename to edit (and optionally a port for node.js). You can use the demo.html file that
104
+ is part of the docs:
105
+
106
+ ```
107
+ $ muedit docs/demo/demo.html
108
+ Server listening at http://localhost:3000
109
+ ```
110
+
111
+ Then open http://localhost:3000 in your browser. The port can be passed to node as `--port <number>`. Alternatively, you
112
+ can run node directly as `node ./bin/muedit.js docs/demo/demo.html`.
113
+
114
+ ## Local Documentation
115
+
116
+ You can also set up the web site docs to test and view locally. The `npm run docs` command copies the `markup-editor.js`
117
+ file and css (which may have changed locally as you develop) to the proper position in the `docs` directory and then runs
118
+ `jsdoc` to produce the API documentation. The API documentation contents and options are defined in `jsdoc.json` and
119
+ use a customized template, `apilayout.tmpl`. The `apireadme.md` in the `docs/resources` directory is displayed as the
120
+ contents of the "home" page.
121
+
122
+ ```
123
+ $ npm run docs
124
+
125
+ > markupeditor@0.9.0 predocs
126
+ > sh predocs.sh && jsdoc -c jsdoc.json
127
+
128
+ Updating ./docs dependencies...
129
+ cp -f ./dist/markup-editor.js ./docs/src/markup-editor.js
130
+ cp -f ./styles/markupeditor.css ./docs/styles/markupeditor.css
131
+ cp -f ./styles/markup.css ./docs/styles/markup.css
132
+ cp -f ./styles/mirror.css ./docs/styles/mirror.css
133
+ cp -f ./styles/toolbar.css ./docs/styles/toolbar.css
134
+
135
+ > markupeditor@0.9.0 docs
136
+ > node ./docs/index.js
137
+
138
+ Server listening at http://localhost:3000
139
+ ```
140
+
141
+ What http://localhost:3000 shows now is the contents hosted at https://stevengharris.github.io/markupeditor-base.
142
+
143
+ Since the project web site is hosted on GitHub Pages, this process makes it simple to keep the docs up-to-date and
144
+ test the web site locally.
145
+
146
+ ## Resources
147
+
148
+ Refer to the [Resources](https://stevengharris.github.io/markupeditor-base/#resources) section of the [project web site](https://stevengharris.github.io/markupeditor-base/)
149
+ for links to documentation, demos, and other projects using the MarkupEditor.
150
+
151
+ ## Acknowledgements
152
+
153
+ This project was originally delivered as a single JavaScript file within the [Swift MarkupEditor](https://github.com/stevengharris/MarkupEditor),
154
+ a project that provides WYSIWYG editing functionality to Swift UIKIt and iOS SwiftUI developers. Eventually, the Swift project moved to using the
155
+ excellent [ProseMirror](https://prosemirror.net) project for all of the heavy lifting of WYSIWYG editing in its web view backend. With the formalization
156
+ of this MarkupEditor base project, the Swift project now depends on `markupeditor-base` which in turn depends on ProseMirror. The MarkupEditor base
157
+ project can then be re-used, within a [desktop editing app](https://github.com/stevengharris/markupeditor-desktop) or
158
+ [VSCode extension](https://github.com/stevengharris/markupeditor-vs).
159
+
160
+ To quote from the ProseMirror site:
161
+
162
+ > ProseMirror is [open source](https://github.com/ProseMirror/prosemirror/blob/master/LICENSE), and you are legally free to use it commercially. Yet, writing, maintaining, supporting, and setting up infrastructure for such a project takes a lot of work and energy. Therefore...
163
+
164
+ > **If you are using ProseMirror to make profit, there is a social expectation that you help fund its maintenance. [Start here](http://marijnhaverbeke.nl/fund/).**
165
+
166
+ I encourage MarkupEditor users to take these statements to heart and support ProseMirror directly if you use the MarkupEditor to make a profit.
167
+
168
+
@@ -0,0 +1,28 @@
1
+ /* Base toast styling */
2
+ .toast {
3
+ font-size: 1rem;
4
+ white-space: nowrap;
5
+ text-overflow: ellipsis;
6
+ display: inline-block;
7
+ visibility: hidden; /* Hidden by default */
8
+ background-color: #333;
9
+ color: #fff;
10
+ text-align: center;
11
+ border-radius: 2px;
12
+ padding: 4px;
13
+ margin-bottom: 8px; /* Spacing between multiple toasts */
14
+ opacity: 0;
15
+ transition: opacity 0.5s, visibility 0.5s;
16
+ }
17
+ @media (prefers-color-scheme: dark) {
18
+ .toast {
19
+ background: #fff;
20
+ color: #333;
21
+ }
22
+ }
23
+
24
+ /* Class added by JavaScript to show the toast */
25
+ .toast.show {
26
+ visibility: visible;
27
+ opacity: 1;
28
+ }
@@ -0,0 +1,126 @@
1
+ import { MU } from "/markup-editor.js"
2
+
3
+ /** A class holding the command items and functionality for a File toolbar that is prepended to the MarkupEditor toolbar */
4
+ class FileToolbar {
5
+ constructor() {
6
+ this.menuItems = this.buildMenuItems()
7
+ }
8
+
9
+ /** SVG defining icons by key, obtained from https://fonts.google.com/icons under https://openfontlicense.org license. */
10
+ icons = {
11
+ // <span class="material-symbols-outlined">note_add</span>
12
+ new: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M440-240h80v-120h120v-80H520v-120h-80v120H320v80h120v120ZM240-80q-33 0-56.5-23.5T160-160v-640q0-33 23.5-56.5T240-880h320l240 240v480q0 33-23.5 56.5T720-80H240Zm280-520v-200H240v640h480v-440H520ZM240-800v200-200 640-640Z"/></svg>',
13
+ // <span class="material-symbols-outlined">file_open</span>
14
+ open: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M240-80q-33 0-56.5-23.5T160-160v-640q0-33 23.5-56.5T240-880h320l240 240v240h-80v-200H520v-200H240v640h360v80H240Zm638 15L760-183v89h-80v-226h226v80h-90l118 118-56 57Zm-638-95v-640 640Z"/></svg>',
15
+ // <span class="material-symbols-outlined">file_copy</span>
16
+ copy: '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M744-192H312q-29 0-50.5-21.5T240-264v-576q0-29 21.5-50.5T312-912h312l192 192v456q0 29-21.5 50.5T744-192ZM576-672v-168H312v576h432v-408H576ZM168-48q-29 0-50.5-21.5T96-120v-552h72v552h456v72H168Zm144-792v195-195 576-576Z"/></svg>'
17
+ }
18
+
19
+ /** Return an array of MenuItems */
20
+ buildMenuItems() {
21
+ let newItem = MU.cmdItem(
22
+ this.newDocument.bind(this),
23
+ {
24
+ enable: () => { return true },
25
+ title: 'New document',
26
+ icon: this.icons.new
27
+ }
28
+ )
29
+ let openItem = MU.cmdItem(
30
+ this.openDocument.bind(this),
31
+ {
32
+ enable: () => { return true },
33
+ title: 'Open document',
34
+ icon: this.icons.open
35
+ }
36
+ )
37
+ let copyItem = MU.cmdItem(
38
+ this.copyHTML.bind(this),
39
+ {
40
+ enable: () => { return true },
41
+ title: 'Copy HTML',
42
+ icon: this.icons.copy,
43
+ // Use a callback to provide a "toast" indicating the copy worked
44
+ callback: this.notifyCopy.bind(this)
45
+ }
46
+ )
47
+ return [newItem, openItem, copyItem]
48
+ }
49
+
50
+ newDocument(state, dispatch, view) {
51
+ MU.setActiveView(view)
52
+ MU.emptyDocument()
53
+ }
54
+
55
+ /** Open the picker as listen for changes in the input element */
56
+ openDocument(state, dispatch, view) {
57
+ MU.setActiveView(view)
58
+ let picker = document.getElementById('docpicker');
59
+ picker.addEventListener('change', this.loadDocument.bind(this));
60
+ picker.showPicker();
61
+ }
62
+
63
+ /**
64
+ * When the user selects an HTML file from the picker, get its contents, update the document in
65
+ * the editor.
66
+ *
67
+ * Note: Because we don't have access to an actual file picker or any way to determine what local
68
+ * directory the file resides in, there is no way to set "base" for the document. This means that
69
+ * local images will almost certainly not load and will generate 404s. Refer to the markupeditor-desktop
70
+ * for an electron example that supports the file system natively.
71
+ */
72
+ loadDocument() {
73
+ let picker = document.getElementById('docpicker');
74
+ picker.removeEventListener('change', this.loadDocument.bind(this));
75
+ let files = picker.files;
76
+ if (files) {
77
+ let file = files[0]
78
+ const reader = new FileReader();
79
+ reader.addEventListener('load', (event) => {
80
+ const text = event.target.result; // The file contents
81
+ MU.setHTML(text)
82
+ });
83
+ reader.readAsText(file);
84
+ }
85
+ }
86
+
87
+ async copyHTML(state, dispatch, view) {
88
+ MU.setActiveView(view)
89
+ const text = MU.getHTML();
90
+ try {
91
+ await navigator.clipboard.writeText(text).then(() => { return })
92
+ } catch (err) {
93
+ console.error('Failed to copy: ', err);
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Briefly add/remove `toast` class to the `menuItem` to display a "toast" notification.
99
+ *
100
+ * The filetoolbar.css defines the CSS using `userstyle` set in muedit.js.
101
+ *
102
+ * @param {HTMLSpanElement} menuItem
103
+ */
104
+ notifyCopy(_ , menuItem) {
105
+ const toast = document.createElement("div");
106
+ toast.classList.add("toast");
107
+ toast.innerText = "Copied HTML"
108
+
109
+ menuItem.appendChild(toast);
110
+
111
+ // Show the toast
112
+ setTimeout(() => {
113
+ toast.classList.add("show");
114
+ }, 10); // Small delay to trigger CSS transition/animation
115
+
116
+ // Automatically hide and remove the toast after a few seconds
117
+ setTimeout(() => {
118
+ toast.classList.remove("show");
119
+ toast.remove();
120
+ }, 1500); // Toast visible for 1.5 seconds
121
+ }
122
+
123
+ }
124
+
125
+ /** By registering the augmentation, we can reference it by name in the <markup-editor> element toolbar attribute */
126
+ MU.registerAugmentation(new FileToolbar())
package/bin/muedit.js ADDED
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * A script to run with node.js to open the MarkupEditor on an HTML file (or empty)
5
+ */
6
+ import {argv} from 'node:process'
7
+ import {parseArgs} from 'node:util'
8
+ import express from 'express'
9
+ import path from 'node:path'
10
+
11
+ const __dirname = import.meta.dirname
12
+
13
+ const app = express()
14
+
15
+ const options = {
16
+ 'port': {
17
+ type: 'string',
18
+ default: '3000'
19
+ }
20
+ }
21
+
22
+ try {
23
+ var { values, positionals } = parseArgs({ argv, options, allowPositionals: true })
24
+ } catch {
25
+ values = []
26
+ positionals = []
27
+ }
28
+
29
+ if ((values.length > 1) || (!values.port) || (positionals.length > 1)) {
30
+ console.log('Usage: muedit [--port number] [<filename>]')
31
+ } else {
32
+ let port = parseInt(values.port)
33
+ let filename = (positionals.length > 0) ? positionals[0] : null
34
+ let filenameAttribute = (filename) ? `filename="${filename}"` : '' // Empty for a new document
35
+ // Note that `base` is set to the filename path automatically
36
+ let placeholder = 'Edit document...'
37
+
38
+ // The web component is always located relative to node's `__dirname`,
39
+ // the location where muedit.js resides, which `app.use(express.static(__dirname))`
40
+ // ensures is available no matter what `process.cwd()` is.
41
+ let webcomponent = `/markup-editor.js`
42
+
43
+ // Allow references to dist from bin to work in index.html, to load the web component
44
+ app.use(express.static(path.join(__dirname, "../dist")))
45
+
46
+ // Allow the userscript and userstyles to load
47
+ app.use(express.static(__dirname))
48
+
49
+ // Allow the relative references inside of the edited document to work
50
+ app.use(express.static(`${process.cwd()}/`, {index: false}))
51
+
52
+ // For parsing application/json
53
+ app.use(express.json())
54
+
55
+ // Load when loading http://localhost:${port}
56
+ app.get('/', (req, res) => {
57
+ res.send(
58
+ `
59
+ <!DOCTYPE html>
60
+ <html lang="en">
61
+ <head>
62
+ <title>${filename ?? "MarkupEditor"}</title>
63
+ <meta name="viewport" charset="utf-8" content="width=device-width, initial-scale=1.0">
64
+ </head>
65
+ <body>
66
+ <markup-editor
67
+ placeholder="${placeholder}"
68
+ ${filenameAttribute}
69
+ userscript="/filetoolbar.js"
70
+ userstyle="/filetoolbar.css"
71
+ prepend="FileToolbar"
72
+ >
73
+ </markup-editor>
74
+ <input id="docpicker" type="file" accept=".html" style="display: none">
75
+ <script src="${webcomponent}" type="module"></script>
76
+ </body>
77
+ </html>
78
+ `
79
+ )
80
+ })
81
+
82
+ app.listen(port, () => {
83
+ console.log(`Server listening at http://localhost:${port}`)
84
+ })
85
+ }