qword 1.0.2 → 1.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 (74) hide show
  1. package/ChangeLog +22 -0
  2. package/README.md +2 -3
  3. package/assets/style.css +10 -10
  4. package/client/_clipboard.js +30 -33
  5. package/client/_clipboard.spec.js +160 -0
  6. package/client/content.d.ts +8 -0
  7. package/client/content.js +14 -0
  8. package/client/content.spec.js +69 -0
  9. package/client/create.d.ts +32 -0
  10. package/client/create.js +95 -0
  11. package/client/create.spec.js +192 -0
  12. package/client/decorations.d.ts +40 -0
  13. package/client/decorations.js +110 -0
  14. package/client/decorations.spec.js +135 -0
  15. package/client/dom.d.ts +7 -0
  16. package/client/dom.js +17 -0
  17. package/client/dom.spec.js +78 -0
  18. package/client/editor.d.ts +72 -0
  19. package/client/editor.js +164 -0
  20. package/client/editor.spec.js +307 -0
  21. package/client/events.d.ts +7 -0
  22. package/client/events.js +8 -0
  23. package/client/events.spec.js +100 -0
  24. package/client/highlight.d.ts +3 -0
  25. package/client/highlight.js +82 -0
  26. package/client/highlight.spec.js +28 -0
  27. package/client/history.d.ts +4 -0
  28. package/client/history.js +16 -0
  29. package/client/history.spec.js +57 -0
  30. package/client/index.d.ts +60 -0
  31. package/client/index.js +37 -0
  32. package/client/loadremote.js +10 -4
  33. package/client/options.d.ts +30 -0
  34. package/client/options.js +59 -0
  35. package/client/options.spec.js +157 -0
  36. package/client/position.d.ts +21 -0
  37. package/client/position.js +33 -0
  38. package/client/position.spec.js +176 -0
  39. package/client/qword.js +141 -391
  40. package/client/save.js +2 -2
  41. package/client/scroll.d.ts +10 -0
  42. package/client/scroll.js +9 -0
  43. package/client/scroll.spec.js +58 -0
  44. package/client/types.d.ts +24 -0
  45. package/dist/qword.js +175 -1
  46. package/dist/qword.js.map +1 -1
  47. package/dist-dev/qword.js +544 -660
  48. package/package.json +30 -15
  49. package/server/edit.js +8 -3
  50. package/server/edit.spec.js +101 -0
  51. package/test/errors.ts +21 -0
  52. package/test/types.ts +134 -0
  53. package/client/codemirror/show-trailing.js +0 -54
  54. package/client/codemirror/use-soft-tabs.js +0 -38
  55. package/client/set-key-map.js +0 -3
  56. package/dist/2b22a33a0a80bb926bbc.png +0 -1
  57. package/dist/8e9019bd86255ac9a618.png +0 -1
  58. package/dist/codemirror.js +0 -2
  59. package/dist/codemirror.js.map +0 -1
  60. package/dist/dword.js +0 -2
  61. package/dist/dword.js.map +0 -1
  62. package/dist-dev/2b22a33a0a80bb926bbc.png +0 -1
  63. package/dist-dev/8e9019bd86255ac9a618.png +0 -1
  64. package/dist-dev/codemirror.js +0 -104
  65. package/dist-dev/dword.js +0 -960
  66. package/modules/codemirror-emmet/dist/emmet.js +0 -48661
  67. package/modules/codemirror-emmet/dist/emmet.min.js +0 -8
  68. package/modules/codemirror-emmet/dist/emmetPlugin.js +0 -840
  69. package/modules/codemirror-emmet/dist/emmetPlugin.min.js +0 -1
  70. package/modules/codemirror-emmet/dist/index.html +0 -46
  71. package/modules/jshint/README.md +0 -114
  72. package/modules/jshint/dist/jshint-rhino.js +0 -32143
  73. package/modules/jshint/dist/jshint.js +0 -32141
  74. package/modules/jshint/package.json +0 -77
package/ChangeLog CHANGED
@@ -1,3 +1,25 @@
1
+ 2026.09.05, v1.1.0
2
+
3
+ fix:
4
+ - 34a981c client: clipboard
5
+
6
+ feature:
7
+ - 83e40e6 qword: cssnano v9.0.2
8
+ - 2f29f72 types: add
9
+ - 58fce83 migrate to rspack
10
+ - 03bb806 qword: @lezer/highlight v1.2.3
11
+ - c5cf54a qword: superc8 v12.7.0
12
+ - 51d8bc5 editor: move out
13
+ - 3d791d7 qword: @putout/plugin-convert-webpack-to-rspack v1.0.1
14
+
15
+ 2026.05.18, v1.0.3
16
+
17
+ fix:
18
+ - 73fb35d qword: clear history on setValueFirst to avoid undo to empty editor
19
+
20
+ feature:
21
+ - 9b3200c qword: cssnano v8.0.1
22
+
1
23
  2026.05.13, v1.0.2
2
24
 
3
25
  feature:
package/README.md CHANGED
@@ -14,13 +14,12 @@ Fork of [edward](https://github.com/cloudcmd/edward "Edward").
14
14
 
15
15
  <img width="601" height="295" alt="image" src="https://github.com/user-attachments/assets/e632f48a-ef74-4d5c-bbe8-8f6e44151d00" />
16
16
 
17
-
18
17
  ## Features
19
18
 
20
19
  - Syntax highlighting based on extension of file for over 90 languages.
21
20
  - Built-in `emmet` (for html files)
22
21
  - Drag n drop (drag file from desktop to editor).
23
- - Configurable options ([json/edit.json][edit.json] could be overriden by `~/.qword.json`)
22
+ - Configurable options ([json/edit.json][edit.json] could be overridden by `~/.qword.json`)
24
23
 
25
24
  ## Install
26
25
 
@@ -52,7 +51,7 @@ Usage: `qword [filename]`
52
51
  ## API
53
52
 
54
53
  qword could be used as middleware for [express](http://expressjs.com "Express").
55
- For this purpuse API could be used.
54
+ For this purpose API could be used.
56
55
 
57
56
  ### Server
58
57
 
package/assets/style.css CHANGED
@@ -1,18 +1,18 @@
1
1
  @font-face {
2
2
  font-family: 'Droid Sans Mono';
3
- src: url(https://themes.googleusercontent.com/static/fonts/droidsansmono/v5/ns-m2xQYezAtqh7ai59hJTwtzT4qNq-faudv5qbO9-U.eot);
3
+ src: url('https://themes.googleusercontent.com/static/fonts/droidsansmono/v5/ns-m2xQYezAtqh7ai59hJTwtzT4qNq-faudv5qbO9-U.eot');
4
4
  src:
5
5
  local('Droid Sans Mono'),
6
6
  local('DroidSansMono'),
7
- url(/font/DroidSansMono.eot) format('embedded-opentype'),
8
- url(https://themes.googleusercontent.com/static/fonts/droidsansmono/v5/ns-m2xQYezAtqh7ai59hJTwtzT4qNq-faudv5qbO9-U.eot?#iefix)
9
- format('embedded-opentype'),
10
- url(https://themes.googleusercontent.com/static/fonts/droidsansmono/v5/ns-m2xQYezAtqh7ai59hJTwtzT4qNq-faudv5qbO9-U.eot)
11
- format('embedded-opentype'),
12
- url(/font/DroidSansMono.woff2) format('woff2'),
13
- url(/font/DroidSansMono.woff) format('woff'),
14
- url(https://themes.googleusercontent.com/static/fonts/droidsansmono/v5/ns-m2xQYezAtqh7ai59hJUYuTAAIFFn5GTWtryCmBQ4.woff)
15
- format('woff'),
7
+ url('/font/DroidSansMono.eot') format('embedded-opentype'),
8
+ url('https://themes.googleusercontent.com/static/fonts/droidsansmono/v5/ns-m2xQYezAtqh7ai59hJTwtzT4qNq-faudv5qbO9-U.eot?#iefix')
9
+ format('embedded-opentype'),
10
+ url('https://themes.googleusercontent.com/static/fonts/droidsansmono/v5/ns-m2xQYezAtqh7ai59hJTwtzT4qNq-faudv5qbO9-U.eot')
11
+ format('embedded-opentype'),
12
+ url('/font/DroidSansMono.woff2') format('woff2'),
13
+ url('/font/DroidSansMono.woff') format('woff'),
14
+ url('https://themes.googleusercontent.com/static/fonts/droidsansmono/v5/ns-m2xQYezAtqh7ai59hJUYuTAAIFFn5GTWtryCmBQ4.woff')
15
+ format('woff'),
16
16
  local('Consolas');
17
17
  font-style: normal;
18
18
  font-weight: 400;
@@ -1,53 +1,50 @@
1
1
  import clipboard from '@cloudcmd/clipboard';
2
- import createElement from '@cloudcmd/create-element';
3
2
  import once from 'once';
4
- import _showMessageOnce from './show-message.js';
3
+ import showMessage from './show-message.js';
5
4
 
6
- const resolve = Promise.resolve.bind(Promise);
7
- const reject = Promise.reject.bind(Promise);
8
- const showMessageOnce = once(_showMessageOnce);
5
+ const showMessageOnce = once(showMessage);
9
6
 
10
- export default (cmd) => {
7
+ export default function clipboardCommand(command) {
8
+ const {_view, _story} = this;
11
9
  const NAME = 'editor-clipboard';
12
- const {_Ace, _story} = this;
13
10
 
14
- const value = _Ace.getSelection('\n');
15
- const insert = (a) => _Ace
16
- .getDoc()
17
- .replaceSelection(a);
11
+ const getSelected = () => {
12
+ const {from, to} = _view.state.selection.main;
13
+ return _view.state.sliceDoc(from, to);
14
+ };
15
+
16
+ const insert = (text) => {
17
+ const {from, to} = _view.state.selection.main;
18
+
19
+ _view.dispatch({
20
+ changes: {
21
+ from,
22
+ to,
23
+ insert: text,
24
+ },
25
+ });
26
+ };
18
27
 
19
- if (cmd === 'copy') {
28
+ if (command === 'copy') {
29
+ const value = getSelected();
20
30
  _story.setData(NAME, value);
31
+
21
32
  return clipboard.writeText(value);
22
33
  }
23
34
 
24
- if (cmd === 'cut') {
35
+ if (command === 'cut') {
36
+ const value = getSelected();
25
37
  _story.setData(NAME, value);
26
- return cut(value) ? resolve() : reject();
38
+ insert('');
39
+
40
+ return clipboard.writeText(value);
27
41
  }
28
42
 
29
43
  return clipboard
30
44
  .readText()
31
45
  .then(insert)
32
46
  .catch(() => {
33
- showMessageOnce('Could not paste from clipboard. Inner buffer used.');
34
-
35
- const value = _story.getData(NAME);
36
-
37
- insert(value);
47
+ showMessageOnce.call(this, 'Could not paste from clipboard. Inner buffer used.');
48
+ insert(_story.getData(NAME));
38
49
  });
39
- };
40
-
41
- function cut(value) {
42
- const textarea = createElement('textarea', {
43
- value,
44
- });
45
-
46
- textarea.select();
47
-
48
- const result = document.execCommand('cut');
49
-
50
- document.body.removeChild(textarea);
51
-
52
- return result;
53
50
  }
@@ -0,0 +1,160 @@
1
+ import {test, stub} from 'supertape';
2
+
3
+ test('clipboard: copy stores value in story', (t) => {
4
+ const story = createStoryStub();
5
+ const view = createViewStub('hello world');
6
+
7
+ const clipboardFn = createClipboard({
8
+ story,
9
+ view,
10
+ });
11
+
12
+ clipboardFn('copy');
13
+
14
+ t.ok(story.setData.called, 'story.setData should be called');
15
+ t.end();
16
+ });
17
+
18
+ test('clipboard: copy calls clipboard.writeText', async (t) => {
19
+ const story = createStoryStub();
20
+ const view = createViewStub('selected text');
21
+
22
+ const writeTextStub = stub().returns(Promise.resolve());
23
+ const clipboardFn = createClipboard({
24
+ story,
25
+ view,
26
+ writeText: writeTextStub,
27
+ });
28
+
29
+ await clipboardFn('copy');
30
+
31
+ t.calledWith(writeTextStub, ['selected text'], 'writeText should be called with selected text');
32
+ t.end();
33
+ });
34
+
35
+ test('clipboard: cut stores value in story', (t) => {
36
+ const story = createStoryStub();
37
+ const view = createViewStub('cut text');
38
+
39
+ const clipboardFn = createClipboard({
40
+ story,
41
+ view,
42
+ });
43
+
44
+ clipboardFn('cut');
45
+
46
+ t.ok(story.setData.called, 'story.setData should be called');
47
+ t.end();
48
+ });
49
+
50
+ test('clipboard: paste reads from clipboard and inserts', async (t) => {
51
+ const story = createStoryStub();
52
+ const view = createViewStub('');
53
+
54
+ const readTextStub = stub().returns(Promise.resolve('pasted content'));
55
+ const clipboardFn = createClipboard({
56
+ story,
57
+ view,
58
+ readText: readTextStub,
59
+ });
60
+
61
+ await clipboardFn('paste');
62
+
63
+ t.ok(view.dispatch.called, 'view.dispatch should be called');
64
+ t.end();
65
+ });
66
+
67
+ test('clipboard: paste falls back to story on clipboard error', async (t) => {
68
+ const story = createStoryStub();
69
+
70
+ story.getData = stub().returns('fallback text');
71
+
72
+ const view = createViewStub('');
73
+
74
+ const readTextStub = stub().returns(Promise.reject(Error('denied')));
75
+ const clipboardFn = createClipboard({
76
+ story,
77
+ view,
78
+ readText: readTextStub,
79
+ });
80
+
81
+ await clipboardFn('paste');
82
+
83
+ t.ok(view.dispatch.called, 'view.dispatch should be called');
84
+ t.end();
85
+ });
86
+
87
+ const createStoryStub = () => ({
88
+ setData: stub(),
89
+ getData: stub().returns(''),
90
+ });
91
+
92
+ const createViewStub = (selectedText = '') => ({
93
+ state: {
94
+ selection: {
95
+ main: {
96
+ from: 0,
97
+ to: selectedText.length,
98
+ },
99
+ },
100
+ sliceDoc: () => selectedText,
101
+ },
102
+ dispatch: stub().returns(true),
103
+ });
104
+
105
+ function createClipboard({story, view, writeText, readText}) {
106
+ const clipboard = {
107
+ writeText: writeText || stub().returns(Promise.resolve()),
108
+ readText: readText || stub().returns(Promise.resolve('')),
109
+ };
110
+
111
+ const showMessageOnce = stub();
112
+
113
+ return (cmd) => {
114
+ const NAME = 'editor-clipboard';
115
+
116
+ const value = view.state.sliceDoc(view.state.selection.main.from, view.state.selection.main.to);
117
+
118
+ const insert = (text) => {
119
+ view.dispatch({
120
+ changes: {
121
+ from: view.state.selection.main.from,
122
+ to: view.state.selection.main.to,
123
+ insert: text,
124
+ },
125
+ });
126
+ };
127
+
128
+ if (cmd === 'copy') {
129
+ story.setData(NAME, value);
130
+ return clipboard.writeText(value);
131
+ }
132
+
133
+ if (cmd === 'cut') {
134
+ story.setData(NAME, value);
135
+ return cut(view) ? Promise.resolve() : Promise.reject();
136
+ }
137
+
138
+ return clipboard
139
+ .readText()
140
+ .then(insert)
141
+ .catch(() => {
142
+ showMessageOnce('Could not paste from clipboard. Inner buffer used.');
143
+
144
+ const fallbackValue = story.getData(NAME);
145
+
146
+ insert(fallbackValue);
147
+ });
148
+ };
149
+ }
150
+
151
+ function cut(view) {
152
+ view.dispatch({
153
+ changes: {
154
+ from: view.state.selection.main.from,
155
+ to: view.state.selection.main.to,
156
+ insert: '',
157
+ },
158
+ });
159
+ return true;
160
+ }
@@ -0,0 +1,8 @@
1
+ import type {EditorView} from '@codemirror/view';
2
+ import type {SourceCode} from './types.js';
3
+
4
+ export function getValue(view: EditorView): SourceCode;
5
+ export const getDocValue: typeof getValue;
6
+
7
+ export function setValue(view: EditorView, value: SourceCode): void;
8
+ export const setDocValue: typeof setValue;
@@ -0,0 +1,14 @@
1
+ export const getValue = (view) => view.state.doc.toString();
2
+ export const getDocValue = getValue;
3
+
4
+ export function setValue(view, value) {
5
+ view.dispatch({
6
+ changes: {
7
+ from: 0,
8
+ to: view.state.doc.length,
9
+ insert: value,
10
+ },
11
+ });
12
+ }
13
+
14
+ export const setDocValue = setValue;
@@ -0,0 +1,69 @@
1
+ import {test} from 'supertape';
2
+ import {EditorState} from '@codemirror/state';
3
+ import {EditorView} from '@codemirror/view';
4
+ import {
5
+ getValue,
6
+ setValue,
7
+ getDocValue,
8
+ setDocValue,
9
+ } from './content.js';
10
+
11
+ function makeView(doc = 'hello') {
12
+ const element = document.createElement('div');
13
+ document.body.appendChild(element);
14
+
15
+ return new EditorView({
16
+ state: EditorState.create({
17
+ doc,
18
+ }),
19
+ parent: element,
20
+ });
21
+ }
22
+
23
+ test('content: getValue returns document string', (t) => {
24
+ const view = makeView('abc');
25
+ const result = getValue(view);
26
+ const expected = 'abc';
27
+
28
+ view.destroy();
29
+
30
+ t.equal(result, expected);
31
+ t.end();
32
+ });
33
+
34
+ test('content: setValue replaces content', (t) => {
35
+ const view = makeView('abc');
36
+ setValue(view, 'xyz');
37
+
38
+ const result = getValue(view);
39
+ const expected = 'xyz';
40
+
41
+ view.destroy();
42
+
43
+ t.equal(result, expected);
44
+ t.end();
45
+ });
46
+
47
+ test('content: getDocValue aliases getValue', (t) => {
48
+ const view = makeView('abc');
49
+ const result = getDocValue(view);
50
+ const expected = 'abc';
51
+
52
+ view.destroy();
53
+
54
+ t.equal(result, expected);
55
+ t.end();
56
+ });
57
+
58
+ test('content: setDocValue aliases setValue', (t) => {
59
+ const view = makeView('abc');
60
+ setDocValue(view, 'xyz');
61
+
62
+ const result = getValue(view);
63
+ const expected = 'xyz';
64
+
65
+ view.destroy();
66
+
67
+ t.equal(result, expected);
68
+ t.end();
69
+ });
@@ -0,0 +1,32 @@
1
+ import {Compartment} from '@codemirror/state';
2
+ import {type ViewUpdate, EditorView} from '@codemirror/view';
3
+ import type {
4
+ SourceCode,
5
+ KeyMap,
6
+ EditorMode,
7
+ EditorTheme,
8
+ } from './types.js';
9
+
10
+ export type CreateEditorOptions = {
11
+ value?: SourceCode;
12
+ mode?: EditorMode | {
13
+ name: EditorMode;
14
+ json?: boolean;
15
+ };
16
+ keyMap?: KeyMap;
17
+ theme?: EditorTheme;
18
+ lineNumbers?: boolean;
19
+ readOnly?: boolean;
20
+ foldGutter?: boolean;
21
+ updateListener?: (update: ViewUpdate) => void;
22
+ };
23
+
24
+ // Augmented EditorView with compartment refs for dynamic option changes.
25
+ export type QwordEditorView = EditorView & {
26
+ _themeCompartment: Compartment;
27
+ _keymapCompartment: Compartment;
28
+ _langCompartment: Compartment;
29
+ _historyCompartment: Compartment;
30
+ };
31
+
32
+ export function createEditor(element: Element, options?: CreateEditorOptions): QwordEditorView;
@@ -0,0 +1,95 @@
1
+ import {EditorState, Compartment} from '@codemirror/state';
2
+ import {
3
+ EditorView,
4
+ lineNumbers as lineNumbersExtension,
5
+ drawSelection,
6
+ keymap,
7
+ } from '@codemirror/view';
8
+ import {history, historyKeymap} from '@codemirror/commands';
9
+ import {
10
+ foldGutter,
11
+ codeFolding,
12
+ syntaxHighlighting,
13
+ } from '@codemirror/language';
14
+ import {highlightStyle} from './highlight.js';
15
+ import {
16
+ keymapExtension,
17
+ themeExtension,
18
+ languageExtension,
19
+ } from './options.js';
20
+ import {markField, lineField} from './decorations.js';
21
+
22
+ export function createEditor(element, options = {}) {
23
+ const {
24
+ value = '',
25
+ mode = 'javascript',
26
+ keyMap = 'default',
27
+ theme = 'default',
28
+ lineNumbers = true,
29
+ readOnly = false,
30
+ foldGutter: fold = false,
31
+ updateListener,
32
+ } = options;
33
+
34
+ const themeCompartment = new Compartment();
35
+ const keymapCompartment = new Compartment();
36
+ const langCompartment = new Compartment();
37
+ const historyCompartment = new Compartment();
38
+
39
+ const hideCursorOnBlur = EditorView.theme({
40
+ '&:not(.cm-focused) .cm-fat-cursor': {
41
+ display: 'none',
42
+ },
43
+ });
44
+
45
+ const extensions = [
46
+ historyCompartment.of([
47
+ history(),
48
+ keymap.of(historyKeymap),
49
+ ]),
50
+ markField,
51
+ lineField,
52
+ themeCompartment.of(themeExtension(theme)),
53
+ keymapCompartment.of(keymapExtension(keyMap)),
54
+ langCompartment.of(languageExtension(mode)),
55
+ syntaxHighlighting(highlightStyle),
56
+ ...lineNumbers
57
+ ? [
58
+ lineNumbersExtension(),
59
+ ]
60
+ : [],
61
+ ...fold
62
+ ? [
63
+ foldGutter(),
64
+ codeFolding(),
65
+ ]
66
+ : [],
67
+ ...readOnly
68
+ ? [
69
+ EditorState.readOnly.of(true),
70
+ ]
71
+ : [],
72
+ ...updateListener
73
+ ? [
74
+ EditorView.updateListener.of(updateListener),
75
+ ]
76
+ : [],
77
+ drawSelection(),
78
+ hideCursorOnBlur,
79
+ ];
80
+
81
+ const view = new EditorView({
82
+ state: EditorState.create({
83
+ doc: value,
84
+ extensions,
85
+ }),
86
+ parent: element,
87
+ });
88
+
89
+ view._themeCompartment = themeCompartment;
90
+ view._keymapCompartment = keymapCompartment;
91
+ view._langCompartment = langCompartment;
92
+ view._historyCompartment = historyCompartment;
93
+
94
+ return view;
95
+ }