svedit 0.1.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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +323 -0
  3. package/dist/AnnotatedStringProperty.svelte +126 -0
  4. package/dist/AnnotatedStringProperty.svelte.d.ts +7 -0
  5. package/dist/AnnotatedStringProperty.svelte.d.ts.map +1 -0
  6. package/dist/CustomProperty.svelte +40 -0
  7. package/dist/CustomProperty.svelte.d.ts +8 -0
  8. package/dist/CustomProperty.svelte.d.ts.map +1 -0
  9. package/dist/Document.svelte.d.ts +191 -0
  10. package/dist/Document.svelte.d.ts.map +1 -0
  11. package/dist/Document.svelte.js +681 -0
  12. package/dist/Node.svelte +41 -0
  13. package/dist/Node.svelte.d.ts +7 -0
  14. package/dist/Node.svelte.d.ts.map +1 -0
  15. package/dist/NodeArrayProperty.svelte +45 -0
  16. package/dist/NodeArrayProperty.svelte.d.ts +7 -0
  17. package/dist/NodeArrayProperty.svelte.d.ts.map +1 -0
  18. package/dist/Svedit.svelte +936 -0
  19. package/dist/Svedit.svelte.d.ts +12 -0
  20. package/dist/Svedit.svelte.d.ts.map +1 -0
  21. package/dist/Transaction.svelte.d.ts +198 -0
  22. package/dist/Transaction.svelte.d.ts.map +1 -0
  23. package/dist/Transaction.svelte.js +577 -0
  24. package/dist/UnknownNode.svelte +12 -0
  25. package/dist/UnknownNode.svelte.d.ts +12 -0
  26. package/dist/UnknownNode.svelte.d.ts.map +1 -0
  27. package/dist/commands.svelte.d.ts +5 -0
  28. package/dist/commands.svelte.d.ts.map +1 -0
  29. package/dist/commands.svelte.js +261 -0
  30. package/dist/index.d.ts +10 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +14 -0
  33. package/dist/types.d.ts +264 -0
  34. package/dist/util.d.ts +4 -0
  35. package/dist/util.d.ts.map +1 -0
  36. package/dist/util.js +78 -0
  37. package/package.json +65 -0
  38. package/src/lib/AnnotatedStringProperty.svelte +126 -0
  39. package/src/lib/CustomProperty.svelte +40 -0
  40. package/src/lib/Document.svelte.js +681 -0
  41. package/src/lib/Node.svelte +41 -0
  42. package/src/lib/NodeArrayProperty.svelte +45 -0
  43. package/src/lib/Svedit.svelte +936 -0
  44. package/src/lib/Transaction.svelte.js +577 -0
  45. package/src/lib/UnknownNode.svelte +12 -0
  46. package/src/lib/commands.svelte.js +261 -0
  47. package/src/lib/index.js +14 -0
  48. package/src/lib/types.d.ts +264 -0
  49. package/src/lib/util.js +78 -0
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "svedit",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "scripts": {
6
+ "dev": "vite dev",
7
+ "build": "vite build",
8
+ "preview": "vite preview",
9
+ "prepare": "svelte-kit sync || echo ''",
10
+ "prepack": "svelte-kit sync && svelte-package && publint",
11
+ "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
12
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
13
+ "format": "prettier --write .",
14
+ "lint": "prettier --check . && eslint .",
15
+ "test:unit": "vitest",
16
+ "test": "npm run test:unit -- --run"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "!dist/**/*.test.*",
21
+ "!dist/**/*.spec.*",
22
+ "src/lib",
23
+ "!src/lib/**/*.test.*",
24
+ "!src/lib/**/*.spec.*"
25
+ ],
26
+ "sideEffects": [
27
+ "**/*.css"
28
+ ],
29
+ "svelte": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "type": "module",
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.ts",
35
+ "svelte": "./dist/index.js"
36
+ }
37
+ },
38
+ "peerDependencies": {
39
+ "svelte": "^5.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "@eslint/compat": "^1.2.5",
43
+ "@eslint/js": "^9.18.0",
44
+ "@sveltejs/adapter-auto": "^6.0.0",
45
+ "@sveltejs/kit": "^2.21.2",
46
+ "@sveltejs/package": "^2.0.0",
47
+ "@sveltejs/vite-plugin-svelte": "^6.0.0",
48
+ "@typescript-eslint/eslint-plugin": "^8.39.1",
49
+ "@typescript-eslint/parser": "^8.39.1",
50
+ "@vitest/browser": "^3.2.4",
51
+ "eslint": "^9.18.0",
52
+ "eslint-config-prettier": "^10.0.1",
53
+ "eslint-plugin-svelte": "^3.0.0",
54
+ "globals": "^16.0.0",
55
+ "prettier": "^3.4.2",
56
+ "prettier-plugin-svelte": "^3.3.3",
57
+ "publint": "^0.3.2",
58
+ "svelte": "^5.0.0",
59
+ "svelte-check": "^4.0.0",
60
+ "vite": "^7.0.4",
61
+ "vitest": "^3.2.3",
62
+ "vitest-browser-svelte": "^1.1.0",
63
+ "nanoid": "^5.1.5"
64
+ }
65
+ }
@@ -0,0 +1,126 @@
1
+ <script>
2
+ import { getContext } from 'svelte';
3
+
4
+ /** @import { AnnotatedStringPropertyProps, AnnotationFragment } from './types.d.ts'; */
5
+
6
+ const svedit = getContext('svedit');
7
+
8
+ /** @type {AnnotatedStringPropertyProps} */
9
+ let {
10
+ path,
11
+ class: css_class,
12
+ placeholder = '',
13
+ } = $props();
14
+
15
+ let is_focused = $derived.by(() => {
16
+ return svedit.doc.selection?.type === 'text' && path.join('.') === svedit.doc.selection?.path.join('.');
17
+ });
18
+
19
+ /**
20
+ * Converts text with annotations into renderable fragments for display.
21
+ * @param {string} text - The plain text content
22
+ * @param {Array<[number, number, string, ...any]>} annotations - Array of annotations where each is [start_offset, end_offset, type, options?] (minimum 3 elements)
23
+ * @returns {Array<string|AnnotationFragment>} Array of fragments - strings for plain text, AnnotationFragment objects for annotated content
24
+ */
25
+ function render_annotated_string(text, annotations) {
26
+ let fragments = [];
27
+ let last_index = 0;
28
+
29
+ // Sort annotations by start_offset
30
+ const sorted_annotations = $state.snapshot(annotations).sort((a, b) => a[0] - b[0]);
31
+
32
+ for (let [index, annotation] of sorted_annotations.entries()) {
33
+ // Add text before the annotation
34
+ if (annotation[0] > last_index) {
35
+ fragments.push(text.slice(last_index, annotation[0]));
36
+ }
37
+
38
+ // Add the annotated string
39
+ const annotated_content = text.slice(annotation[0], annotation[1]);
40
+ fragments.push({
41
+ type: annotation[2],
42
+ content: annotated_content,
43
+ annotation_index: index,
44
+ data: annotation[3]
45
+ });
46
+
47
+ last_index = annotation[1];
48
+ }
49
+
50
+ // Add any remaining text after the last annotation
51
+ if (last_index < text.length) {
52
+ fragments.push(text.slice(last_index));
53
+ }
54
+
55
+ return fragments;
56
+ }
57
+
58
+ let fragments = $derived(render_annotated_string(svedit.doc.get(path)[0], svedit.doc.get(path)[1]));
59
+ let plain_text = $derived(svedit.doc.get(path)[0]);
60
+
61
+ /**
62
+ * @param {MouseEvent} e - The click event
63
+ */
64
+ function handle_link_click(e) {
65
+ e.preventDefault();
66
+ }
67
+ </script>
68
+
69
+ <!-- ATTENTION: The comments are needed to prevent unwanted text nodes with whitespace. -->
70
+ <div
71
+ data-type="text"
72
+ data-path={path.join('.')}
73
+ style="anchor-name: --{path.join('-')};"
74
+ class="text svedit-selectable {css_class}"
75
+ class:empty={plain_text.length === 0}
76
+ class:focused={is_focused}
77
+ placeholder={placeholder}
78
+ >
79
+ {#each fragments as fragment, index (index)}
80
+ {#if typeof fragment === 'string'}<!--
81
+ -->{fragment}<!--
82
+ -->{:else if fragment.type === 'emphasis'}<!--
83
+ --><em>{fragment.content}</em><!--
84
+ -->{:else if fragment.type === 'strong'}<!--
85
+ --><strong>{fragment.content}</strong><!--
86
+ -->{:else if fragment.type === 'link'}<!--
87
+ --><a
88
+ onclick={handle_link_click}
89
+ style="anchor-name: --{path.join('-') + '-' + fragment.annotation_index};"
90
+ href={fragment.data.href}
91
+ target={fragment.data.target || '_self'}>{fragment.content}</a><!--
92
+ -->{:else}<!--
93
+ -->{fragment.content}<!--
94
+ -->{/if}
95
+ {/each}<!--
96
+ --><br>
97
+ </div>
98
+
99
+ <style>
100
+ .text {
101
+ white-space: pre-wrap;
102
+ overflow-wrap: anywhere;
103
+ box-sizing: content-box;
104
+
105
+ &.heading1 {
106
+ text-wrap: var(--text-wrap);
107
+ }
108
+ }
109
+
110
+ [placeholder].empty::before {
111
+ content: attr(placeholder);
112
+ pointer-events: none;
113
+ color: color-mix(in oklch, currentcolor 50%, transparent);
114
+ }
115
+
116
+ /* We can safely hide the <br> element when the placeholder is empty and focused. */
117
+ /* No longer the cursor will be rendered after the placeholder when focused. */
118
+ [placeholder].empty.focused br {
119
+ display:none;
120
+ }
121
+
122
+ .text.focused {
123
+ background: none;
124
+ outline: 1px dashed var(--editing-stroke-color);
125
+ }
126
+ </style>
@@ -0,0 +1,40 @@
1
+ <script>
2
+ /** @import { CustomPropertyProps } from './types.d.ts'; */
3
+
4
+ /** @type {CustomPropertyProps} */
5
+ let {
6
+ path,
7
+ class: css_class,
8
+ children,
9
+ } = $props();
10
+ </script>
11
+
12
+ <!-- NOTE: It's super crucial that there is no extra whitespace, to not introduce additional cursor positions in contenteditable -->
13
+ <div
14
+ class="property {css_class}"
15
+ data-type="property"
16
+ data-path={path.join('.')}
17
+ style="anchor-name: --{path.join('-')};"
18
+ >
19
+ <div class="cursor-trap svedit-selectable"><br/></div>
20
+ <div class="property-content" contenteditable="false">{@render children()}</div>
21
+ </div>
22
+
23
+ <style>
24
+ .property {
25
+ position: relative;
26
+ }
27
+ .cursor-trap {
28
+ position: absolute;
29
+ top: 0;
30
+ left: 0;
31
+ right: 0;
32
+ bottom: 0;
33
+ z-index: 1;
34
+ outline: none;
35
+ }
36
+ .property-content {
37
+ width: 100%;
38
+ height: 100%;
39
+ }
40
+ </style>