From 3b7e7606bba816227929200f9a449a96fc09e784 Mon Sep 17 00:00:00 2001 From: Steffen Becker Date: Sat, 4 Jul 2026 17:09:12 +0200 Subject: [PATCH] feat: bullet symbols --- CHANGELOG.md | 1 + src/App.svelte | 6 +- src/assets/fonts/EdenSymbols.woff2 | Bin 0 -> 2244 bytes src/assets/fonts/LICENSE-DejaVu.txt | 78 ++++++++++++ src/lib/components/Toolbar.svelte | 134 ++++++++++++++++--- src/lib/editor/extensions.ts | 2 +- src/lib/editor/extensions/bulletList.ts | 65 ++++++++++ src/lib/export/docx.ts | 14 +- src/lib/export/odt.ts | 163 ++++++++++++++++++++---- src/lib/export/pdf.ts | 39 +++++- src/lib/import/docx.ts | 6 +- src/lib/import/docxStyles.ts | 7 +- src/lib/import/odt.ts | 19 ++- src/lib/utils/bulletListTypes.ts | 92 +++++++++++++ src/styles/editor.css | 17 +++ src/styles/global.css | 12 ++ tests/docx-roundtrip.test.ts | 22 +++- tests/roundtrip.test.ts | 25 +++- tests/unit/bullet-list-types.test.ts | 92 +++++++++++++ tests/unit/docx-export.test.ts | 5 +- 20 files changed, 735 insertions(+), 64 deletions(-) create mode 100644 src/assets/fonts/EdenSymbols.woff2 create mode 100644 src/assets/fonts/LICENSE-DejaVu.txt create mode 100644 src/lib/editor/extensions/bulletList.ts create mode 100644 src/lib/utils/bulletListTypes.ts create mode 100644 tests/unit/bullet-list-types.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a44ec6..69df8bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ - Line spacing and paragraph spacing (space before / after) - Increase / decrease indent (paragraphs and lists) - Bulleted and ordered lists — nested, with multiple numbering styles (decimal, alpha, roman; `.` or `)` suffix) and whole-list indent; Tab / Shift-Tab to nest / un-nest +- Customizable bullet symbols per list level (Word-style picker on the bullet-list split button: • ◦ ▪ ❖ ➢ ⇨ ✓ – >); round-trips to ODF `text:bullet-char` and DOCX `w:lvlText`, and the DOCX/ODT import maps Wingdings/Symbol bullets (arrows, diamonds, checkmarks …) to their Unicode equivalents instead of flattening them to plain dots. Symbols Liberation Serif lacks render from a bundled 2 KB DejaVu Sans subset (`EdenSymbols.woff2`), so markers look compact and identical on every platform instead of a stretched OS fallback - Tab stops (real tab character) and manual line breaks (Shift+Enter) **Insert** diff --git a/src/App.svelte b/src/App.svelte index 0bec8ee..2cb2c86 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -627,11 +627,11 @@
Save / Export
{/if} diff --git a/src/assets/fonts/EdenSymbols.woff2 b/src/assets/fonts/EdenSymbols.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..f37f9d3aeb894a426528a78b5cb6c1eefbb8bf1c GIT binary patch literal 2244 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 SDRuw${pzsz*HVS4JWl|NwmcdD literal 0 HcmV?d00001 diff --git a/src/assets/fonts/LICENSE-DejaVu.txt b/src/assets/fonts/LICENSE-DejaVu.txt new file mode 100644 index 0000000..b3d93a1 --- /dev/null +++ b/src/assets/fonts/LICENSE-DejaVu.txt @@ -0,0 +1,78 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: DejaVu fonts +Upstream-Author: Stepan Roh (original author), + see /usr/share/doc/fonts-dejavu-core/AUTHORS for full list +Source: https://dejavu-fonts.github.io/ + +Files: * +Copyright: Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. + Bitstream Vera is a trademark of Bitstream, Inc. + DejaVu changes are in public domain. +License: bitstream-vera + Permission is hereby granted, free of charge, to any person obtaining a copy + of the fonts accompanying this license ("Fonts") and associated + documentation files (the "Font Software"), to reproduce and distribute the + Font Software, including without limitation the rights to use, copy, merge, + publish, distribute, and/or sell copies of the Font Software, and to permit + persons to whom the Font Software is furnished to do so, subject to the + following conditions: + . + The above copyright and trademark notices and this permission notice shall + be included in all copies of one or more of the Font Software typefaces. + . + The Font Software may be modified, altered, or added to, and in particular + the designs of glyphs or characters in the Fonts may be modified and + additional glyphs or characters may be added to the Fonts, only if the fonts + are renamed to names not containing either the words "Bitstream" or the word + "Vera". + . + This License becomes null and void to the extent applicable to Fonts or Font + Software that has been modified and is distributed under the "Bitstream + Vera" names. + . + The Font Software may be sold as part of a larger software package but no + copy of one or more of the Font Software typefaces may be sold by itself. + . + THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, + TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME + FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING + ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF + THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE + FONT SOFTWARE. + . + Except as contained in this notice, the names of Gnome, the Gnome + Foundation, and Bitstream Inc., shall not be used in advertising or + otherwise to promote the sale, use or other dealings in this Font Software + without prior written authorization from the Gnome Foundation or Bitstream + Inc., respectively. For further information, contact: fonts at gnome dot + org. + +Files: debian/* +Copyright: (C) 2005-2006 Peter Cernak + (C) 2006-2011 Davide Viti + (C) 2011-2013 Christian Perrier + (C) 2013 Fabian Greffrath +License: GPL-2+ + This program is free software; you can redistribute it + and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later + version. + . + This program is distributed in the hope that it will be + useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + PURPOSE. See the GNU General Public License for more + details. + . + You should have received a copy of the GNU General Public + License along with this package; if not, write to the Free + Software Foundation, Inc., 51 Franklin St, Fifth Floor, + Boston, MA 02110-1301 USA + . + On Debian systems, the full text of the GNU General Public + License version 2 can be found in the file + /usr/share/common-licenses/GPL-2'. diff --git a/src/lib/components/Toolbar.svelte b/src/lib/components/Toolbar.svelte index 66ca714..1c172a8 100644 --- a/src/lib/components/Toolbar.svelte +++ b/src/lib/components/Toolbar.svelte @@ -3,6 +3,7 @@ import HistoryButton from './HistoryButton.svelte'; import AlignButton from './AlignButton.svelte'; import { ORDERED_LIST_TYPES, DEFAULT_ORDERED_TYPE, type OrderedListType } from '../utils/orderedListTypes'; + import { BULLET_TYPES } from '../utils/bulletListTypes'; import { isInHeaderCell } from '../editor/extensions/tableHeaderRow'; let { editor, tick }: { editor: Editor | null; tick: number } = $props(); @@ -33,6 +34,30 @@ }); let olMenuOpen = $state(false); + let blMenuOpen = $state(false); + + // Marker char of the innermost bullet list at the cursor (null = default cycle + // or not in a bullet list). + let currentBulletChar = $derived.by(() => { + if (tick < 0 || !editor || !editor.isActive('bulletList')) return null; + return (editor.getAttributes('bulletList').bulletChar ?? null) as string | null; + }); + + // Toggle a plain bullet list — the split button's main half. + function toggleBulletList() { + blMenuOpen = false; + editor?.chain().focus().toggleBulletList().run(); + } + + // Apply a marker char to the innermost list level (null = default); creates the + // list first if the selection isn't in one. + function applyBulletChar(char: string | null) { + if (!editor) return; + blMenuOpen = false; + const chain = editor.chain().focus(); + if (!editor.isActive('bulletList')) chain.toggleBulletList(); + chain.setBulletChar(char).run(); + } // Toggle a plain (decimal) ordered list — the split button's main half. function toggleOrderedList() { @@ -49,9 +74,9 @@ chain.updateAttributes('orderedList', { listStyleType: key }).run(); } - function olMenuClickOutside(node: HTMLElement) { + function menuClickOutside(node: HTMLElement, close: () => void) { function handler(e: MouseEvent) { - if (!node.contains(e.target as Node)) olMenuOpen = false; + if (!node.contains(e.target as Node)) close(); } window.addEventListener('mousedown', handler); return { destroy() { window.removeEventListener('mousedown', handler); } }; @@ -132,21 +157,65 @@
- -
+
(blMenuOpen = false)}> +
+ + +
+ {#if blMenuOpen} + + {/if} +
+
(olMenuOpen = false)}>