diff --git a/calcit.cirru b/calcit.cirru index fcb799f..0168417 100644 --- a/calcit.cirru +++ b/calcit.cirru @@ -6,12 +6,12 @@ {} (:description |Browser-app) (:init-fn 'text-diff.main/main!) (:mode :js) (:reload-fn 'text-diff.main/reload!) (:target :browser) :feature-policy $ {} :modules $ [] |respo.calcit/ |respo-ui.calcit/ |respo-markdown.calcit/ |reel.calcit/ |js-ffi/ - :type-slots $ {} $ :dispatch-op 'text-diff.schema/DispatchOp + :type-slots $ {} $ :dispatch-op 'text-diff.schema/Op :files $ {} 'text-diff.comp.container $ %{} 'FileEntry :defs $ {} $ 'comp-container %{} 'CodeEntry (:doc |) - :code $ quote $ defcomp comp-container (reel) + :code $ quote $ defcomp comp-container (reel on-control) let store $ :store reel states $ :states store @@ -43,10 +43,12 @@ -> Unit println $ :content store if dev? - Option :some $ comp-typed-reel (>> states :reel) reel $ {} + Option :some $ comp-typed-reel (>> states :reel) reel ({}) on-control Option :none :examples $ [] - :schema $ quote $ Fn (reel.typed/State text-diff.schema/Op text-diff.schema/Store) (-> respo.schema/Component) + :schema $ quote $ Fn (reel.typed/State text-diff.schema/Op text-diff.schema/Store) + Fn reel.typed/Control $ -> Unit + -> respo.schema/Component :ns $ %{} 'NsEntry (:doc |) :code $ quote $ ns text-diff.comp.container :require (respo-ui.core :as ui) @@ -98,17 +100,13 @@ 'dispatch! $ %{} 'CodeEntry (:doc |) :code $ quote $ defn dispatch! (op) when config/dev? $ println |Dispatch: op - match (schema/decode-op op) - (:some resolved) - match resolved - (reel.typed/Control control) - reset! *reel $ typed/apply-control updater @*reel control - (text-diff.schema/Op action) - reset! *reel $ typed/record-op updater @*reel action (generate-id!) (shared/now-ms) - (:none) &unit + reset! *reel $ match op + (reel.typed/Control control) (typed/apply-control updater @*reel control) + (text-diff.schema/Op action) + typed/record-op updater @*reel action (generate-id!) (shared/now-ms) , &unit :examples $ [] - :schema $ quote $ Fn AnyEnum (-> Unit) (:features :js-ffi) + :schema $ quote $ Fn text-diff.schema/ResolvedOp (-> Unit) (:features :js-ffi) 'main! $ %{} 'CodeEntry (:doc |) :code $ quote $ defn main! () println "|Running mode:" $ if config/dev? |dev |release @@ -117,11 +115,11 @@ hint-fn $ Fn (reel.typed/State text-diff.schema/Op text-diff.schema/Store) (reel.typed/State text-diff.schema/Op text-diff.schema/Store) (-> Unit) render-app! , &unit - listen-devtools! |a dispatch! + listen-devtools! |a on-control! match browser/storage-get $ .unwrap $ get config/site :storage-key (:some raw) - dispatch! $ :: :hydrate-storage $ parse-cirru-edn raw + dispatch! $ schema/Op :hydrate-storage $ schema/decode-store (parse-cirru-edn raw) (:none) &unit browser/add-event-listener! |beforeunload $ fn (event) (persist-storage!) println "|App started." @@ -132,6 +130,10 @@ .unwrap $ respo-browser/query-selector |.app :examples $ [] :schema $ quote $ Fn (-> respo.dom/DomElement) (:features :js-ffi) + 'on-control! $ %{} 'CodeEntry (:doc |) + :code $ quote $ defn on-control! (control) (dispatch! control) + :examples $ [] + :schema $ quote $ Fn reel.typed/Control (-> Unit) 'persist-storage! $ %{} 'CodeEntry (:doc |) :code $ quote $ defn persist-storage! () browser/storage-set! @@ -157,8 +159,10 @@ 'render-app! $ %{} 'CodeEntry (:doc |) :code $ quote $ defn render-app! () render-with! (mount-target) - fn () $ comp-container @*reel - , dispatch! + fn () $ comp-container @*reel on-control! + fn (op) + hint-fn $ Fn text-diff.schema/Op $ -> Unit + dispatch! op :examples $ [] :schema $ quote $ Fn (-> Unit) (:features :js-ffi) 'snippets $ %{} 'CodeEntry (:doc |) @@ -187,11 +191,6 @@ :code $ quote $ defunion CursorKey Tag String Number :examples $ [] :schema $ quote UnionDef - 'DispatchOp $ %{} 'CodeEntry - :doc "|Respo dispatch slot accepts application operations and Reel controls." - :code $ quote $ def DispatchOp (:: :unhandled) - :examples $ [] - :schema $ quote AnyEnum 'Op $ %{} 'CodeEntry (:doc |) :code $ quote $ defenum Op :states (List text-diff.schema/CursorKey) Dynamic @@ -209,30 +208,13 @@ :content String :examples $ [] :schema $ quote StructDef - 'decode-op $ %{} 'CodeEntry (:doc |) - :code $ quote $ defn decode-op (op) - let - some-action $ fn (action) - hint-fn $ Fn text-diff.schema/ResolvedOp $ -> (Option text-diff.schema/ResolvedOp) - Option :some action - match (reel.typed/decode-control op) - (:some control) (some-action control) - (:none) - match op - (:states cursor data) - some-action $ Op :states - decode-map-as cursor $ List text-diff.schema/CursorKey - , data - (:content content) - some-action $ Op :content $ decode-map-as content String - (:hydrate-storage data) - some-action $ Op :hydrate-storage $ decode-map-as - if (struct? data) (&struct:to-map data) data - , text-diff.schema/Store - _ $ do (eprintln "|Unknown op:" op) (Option :none) + 'decode-store $ %{} 'CodeEntry (:doc |) + :code $ quote $ defn decode-store (data) + decode-map-as + if (struct? data) (&struct:to-map data) data + , text-diff.schema/Store :examples $ [] - :schema $ quote $ Fn AnyEnum - -> $ Option text-diff.schema/ResolvedOp + :schema $ quote $ Fn Input (-> text-diff.schema/Store) (:generics Input) 'store $ %{} 'CodeEntry (:doc |) :code $ quote $ def store Store :states ({}) :content | diff --git a/deps.cirru b/deps.cirru index fda9421..45ac9bc 100644 --- a/deps.cirru +++ b/deps.cirru @@ -1,7 +1,7 @@ {} (:calcit-version |0.38.0) :version |0.1.4 - :dependencies $ {} (|Respo/reel.calcit |0.10.0) + :dependencies $ {} (|Respo/reel.calcit |0.11.0) |Respo/respo-markdown.calcit |0.8.2 |Respo/respo-ui.calcit |0.15.2 |Respo/respo.calcit |0.19.5 diff --git a/scripts/upgrade.test.mjs b/scripts/upgrade.test.mjs index 7ffdd1f..c1d2f9e 100644 --- a/scripts/upgrade.test.mjs +++ b/scripts/upgrade.test.mjs @@ -2,43 +2,47 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import * as c from '../js-out/calcit.core.mjs'; import { updater } from '../js-out/text-diff.updater.mjs'; -import { decode_op, store } from '../js-out/text-diff.schema.mjs'; -import { Control } from '../js-out/reel.typed.mjs'; +import { decode_store, Op, store } from '../js-out/text-diff.schema.mjs'; +import { Control, apply_control, new_reel, record_op } from '../js-out/reel.typed.mjs'; -const tags = c.init_tags(['content', 'states']); +const tags = c.init_tags(['content', 'states', 'hydrate-storage', 'editor', 'data', 'draft', 'toggle', 'display?', 'records']); const read = (value, key) => c.option_$o_unwrap(c.get(value, key)); -const apply = (source, op) => updater(source, c.option_$o_unwrap(decode_op(c.parse_cirru_edn(op))), 'test-op', 0); +const op = (tag, ...values) => c._PCT__$o__$o_(Op, tag, ...values); +const apply = (source, tag, ...values) => updater(source, op(tag, ...values), 'test-op', 0); test('content update preserves cursor state and leaves original store unchanged', () => { - const updated = apply(store, ':: :content |hello'); + const updated = apply(store, tags.content, 'hello'); assert.equal(read(updated, tags.content), 'hello'); assert.equal(read(store, tags.content), ''); assert.equal(read(updated, tags.states), read(store, tags.states)); }); test('hydration preserves the persisted map format', () => { - const updated = apply(store, ':: :hydrate-storage $ {} (:content |restored) (:states $ {})'); + const restored = decode_store(c.parse_cirru_edn('{} (:content |restored) (:states $ {})')); + const updated = apply(store, tags['hydrate-storage'], restored); assert.equal(read(updated, tags.content), 'restored'); const roundTrip = c.parse_cirru_edn(c.format_cirru_edn(updated)); assert.equal(read(roundTrip, tags.content), 'restored'); }); test('cursor updates preserve content', () => { - const populated = apply(store, ':: :content |keep'); - const updated = apply(populated, ':: :states ([] :editor) $ {} (:draft |typing)'); + const populated = apply(store, tags.content, 'keep'); + const updated = apply(populated, tags.states, c._$L_(tags.editor), c._$n__$M_(tags.draft, 'typing')); assert.equal(read(updated, tags.content), 'keep'); - const editor = read(read(updated, tags.states), c.init_tags(['editor']).editor); - assert.equal(read(read(editor, c.init_tags(['data']).data), c.init_tags(['draft']).draft), 'typing'); - for (const [key, value] of [['|中文', '中文'], ['0', 0]]) { - const keyed = apply(populated, `:: :states ([] :editor ${key}) false`); - const nested = read(read(read(keyed, tags.states), c.init_tags(['editor']).editor), value); - assert.equal(read(nested, c.init_tags(['data']).data), false); + const editor = read(read(updated, tags.states), tags.editor); + assert.equal(read(read(editor, tags.data), tags.draft), 'typing'); + for (const key of ['中文', 0]) { + const keyed = apply(populated, tags.states, c._$L_(tags.editor, key), false); + const nested = read(read(read(keyed, tags.states), tags.editor), key); + assert.equal(read(nested, tags.data), false); assert.equal(read(keyed, tags.content), 'keep'); } - assert.throws(() => apply(populated, ':: :states ([] :editor false) $ {}'), /CursorKey/); + assert.throws(() => apply(populated, tags.states, c._$L_(tags.editor, false), c._$n__$M_()), /CursorKey|declared type/); }); -test('legacy Reel controls decode to the nominal control member', () => { - const control = c.option_$o_unwrap(decode_op(c.parse_cirru_edn(':: :reel/toggle'))); - assert.equal(c.option_$o_unwrap(c._$n_enum_$o_definition(control)), Control); +test('typed Reel control toggles the panel without adding an edit', () => { + const before = record_op(updater, new_reel(store), op(tags.content, 'hello'), 'test-op', 0); + const after = apply_control(updater, before, c._PCT__$o__$o_(Control, tags.toggle)); + assert.equal(read(after, tags['display?']), !read(before, tags['display?'])); + assert.equal(read(after, tags.records), read(before, tags.records)); });