yrby 0.8.0 → 0.8.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e731133b862944bb0475e1175d59c1d365924a5da722a395624426044371413
4
- data.tar.gz: 1ec6b9298ecb11609d3d93279ecd5239f54b10ae5e7cd5f9d1b7deffe75288b6
3
+ metadata.gz: ea4b274bb5d8928c48900e3fe08b1e2b5d93c7e9970fe8ee6d453366e2c2c9be
4
+ data.tar.gz: 6df2e03ac56e522e1ba854266dd193124514d0fe5f756ad79f1038e4704103b9
5
5
  SHA512:
6
- metadata.gz: 3b0d016da20f2bd97529fdf0fce808a771e4997d9834f5be9234ef6f0225485f72fa3854cd7a362ff72275f84f3fa8003ac0cd42efea55028c07879fa6b9da1a
7
- data.tar.gz: da77acc0583feb5e7cbb4da0076c6fd3053c11fb64a6aa4c33d708a31220a49e67f1cadf6c5ea27f67c9dd5b9c655285a004dbe70acea1133a232c606d8f7ccb
6
+ metadata.gz: e45d38cf54bf43b63a72e0127c89aedd3f5c713a7677afddbdd5c2b4c702af1d755bc1c15fdf22d0f118d9611dc7dd06843efb7ec94cb89d6a898cf6f6d44b30
7
+ data.tar.gz: 651268d3b107633e104e346840083bcf5b1ecda00ae3d4586ed89cdb235b8bda34df34c6d60fcde23fdd47b18d0439ff9fca5e36683322799fc66d86157923bb
data/CHANGELOG.md CHANGED
@@ -6,6 +6,37 @@ to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.8.1] - 2026-10-03
10
+
11
+ ### Added
12
+
13
+ - `unknown_types` on `Y::Lexical` and `Y::ProseMirror`, and so on `Y::Lexxy`
14
+ and `Y::Tiptap`. It lists the node types in a document that neither the
15
+ built-in schema nor a rule handles, which are the ones `to_html` can't fully
16
+ render. Render a real document from your editor in a test and assert the list
17
+ is empty.
18
+ - The renderer crates export `escape_text` and `escape_attr` for callers that
19
+ build their own markup (`lexical-yjs-html` 0.1.4, `prosemirror-yjs-html`
20
+ 0.1.5).
21
+
22
+ ### Changed
23
+
24
+ - The demo app and the render-parity test run on Lexxy 1.0. `Y::Lexxy`
25
+ output matches Lexxy 1.0's own `value`, including image alt text edited
26
+ after the document syncs.
27
+
28
+ ### Fixed
29
+
30
+ - The Lexical renderers keep the text of an unknown inline wrapper, such as a
31
+ mark with no rule. The text renders without the wrapper. Before, the whole
32
+ node rendered as nothing, so its text was missing from the HTML.
33
+ - `Y::Lexxy`'s list-item rule escapes the stored `__checked` and `__value`
34
+ attributes. Collaborators write those attributes, and a crafted value could
35
+ break out of the HTML attribute. Normal values (booleans and integers) render
36
+ the same as before.
37
+ - The crate READMEs' callback examples escape the values they insert, and both
38
+ READMEs describe how unknown node types render.
39
+
9
40
  ## [0.8.0] - 2026-10-01
10
41
 
11
42
  This release ships alongside yrby-client 0.6.0, and the client entries below
data/Cargo.lock CHANGED
@@ -229,7 +229,7 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
229
229
 
230
230
  [[package]]
231
231
  name = "lexical-yjs-html"
232
- version = "0.1.3"
232
+ version = "0.1.4"
233
233
  dependencies = [
234
234
  "serde_json",
235
235
  "yjs-html-core",
@@ -348,7 +348,7 @@ dependencies = [
348
348
 
349
349
  [[package]]
350
350
  name = "prosemirror-yjs-html"
351
- version = "0.1.4"
351
+ version = "0.1.5"
352
352
  dependencies = [
353
353
  "serde_json",
354
354
  "yjs-html-core",
data/README.md CHANGED
@@ -338,12 +338,12 @@ every editor configures its own. `Y::Lexical` is the core Lexical base
338
338
  (paragraphs, headings, quotes, code, lists, tables, links, and the full
339
339
  text-format model), and other Lexical editors extend it with rules.
340
340
 
341
- It handles every node in the Lexxy 0.9.x set: paragraphs, headings, every text
342
- format and their combinations, links, the four list types with nesting,
343
- blockquotes, code blocks, tabs and soft breaks, horizontal rules, tables with
344
- header cells, image galleries, and ActionText attachments. Uploads and mentions
345
- both render as `<action-text-attachment>` elements, which ActionText can
346
- re-render.
341
+ It handles every node in Lexxy 1.0, which has the same set as 0.9.x:
342
+ paragraphs, headings, every text format and their combinations, links, the
343
+ four list types with nesting, blockquotes, code blocks, tabs and soft breaks,
344
+ horizontal rules, tables with header cells, image galleries, and ActionText
345
+ attachments. Uploads and mentions both render as `<action-text-attachment>`
346
+ elements, which ActionText can re-render.
347
347
 
348
348
  The Lexical support has the same two layers. `Y::Lexical` handles core Lexical
349
349
  structure natively, and everything Lexxy adds is in `Y::Lexxy`'s rule set,
@@ -401,6 +401,13 @@ attribute names your templates and blocks will read, and `children` and `text`
401
401
  tell you which `contains:` to pick. Child block types mean `:blocks`, and text
402
402
  means `:inline`.
403
403
 
404
+ `unknown_types` lists the types whose `handled` is nil. `to_html` still
405
+ renders them as well as it can. An unknown container or inline wrapper renders
406
+ its children without its own markup, so its text stays. A node that keeps its
407
+ content only in its attributes renders nothing. Either way, the node's markup is
408
+ missing from the HTML, and nothing raises. When you add editor nodes, add a test
409
+ that renders a real document and asserts `unknown_types` is empty.
410
+
404
411
  When a declarative rule can't express the markup, give the node a block:
405
412
 
406
413
  ```ruby
@@ -3,7 +3,7 @@ name = "lexical-yjs-html"
3
3
  description = "Render Lexical-shaped yrs documents to HTML, no browser or Node required"
4
4
  repository = "https://github.com/jpcamara/yrby"
5
5
  readme = "README.md"
6
- version = "0.1.3"
6
+ version = "0.1.4"
7
7
  edition = "2024"
8
8
  rust-version = "1.85"
9
9
  authors = ["JP Camara <johnpcamara@gmail.com>"]
@@ -697,8 +697,15 @@ fn render_inline<T: ReadTxn>(
697
697
  None => render_link(txn, &child, depth, em, rules),
698
698
  }
699
699
  } else if ruled_inline && !is_builtin(&ty) {
700
- if let Some(rule) = rules.nodes.get(ty.as_str()) {
701
- render_rule_inline(txn, &child, &ty, rule, depth, em, rules);
700
+ match rules.nodes.get(ty.as_str()) {
701
+ Some(rule) => render_rule_inline(txn, &child, &ty, rule, depth, em, rules),
702
+ // An unknown inline wrapper, such as a mark with no
703
+ // rule, renders its text without the wrapper, the same
704
+ // way unknown blocks do. Its text is kept.
705
+ None if depth < MAX_INLINE_DEPTH => {
706
+ render_inline(txn, &child, depth + 1, ruled_inline, em, rules);
707
+ }
708
+ None => {}
702
709
  }
703
710
  }
704
711
  // Nested blocks are rendered by their parent block's renderer.
@@ -956,15 +963,18 @@ fn elem_type<T: ReadTxn>(txn: &T, e: &XmlElementRef) -> String {
956
963
  }
957
964
 
958
965
  /// Text-content escaping, matching what the browser's serializer emits:
959
- /// `&`, `<`, `>` escaped; quotes left alone in text.
960
- fn escape_text(s: &str) -> String {
966
+ /// `&`, `<`, `>` escaped; quotes left alone in text. Public for splice
967
+ /// callers. Values read from a deferred segment's attributes come from
968
+ /// collaborators, so escape anything you put into markup.
969
+ pub fn escape_text(s: &str) -> String {
961
970
  s.replace('&', "&amp;")
962
971
  .replace('<', "&lt;")
963
972
  .replace('>', "&gt;")
964
973
  }
965
974
 
966
- /// Attribute-value escaping: text escaping plus `"`.
967
- fn escape_attr(s: &str) -> String {
975
+ /// Attribute-value escaping: text escaping plus `"`. Public for splice
976
+ /// callers, for document values interpolated into attribute positions.
977
+ pub fn escape_attr(s: &str) -> String {
968
978
  escape_text(s).replace('"', "&quot;")
969
979
  }
970
980
 
@@ -1140,6 +1150,28 @@ mod tests {
1140
1150
  assert!(html.contains("<p>Before gallery</p>"));
1141
1151
  }
1142
1152
 
1153
+ /// An unknown inline wrapper never drops its text. The fixture is a
1154
+ /// real capture: two collaborating editors typed "Collab #ruby rocks
1155
+ /// and #peer too", the word "rocks" wrapped in @lexical/mark's
1156
+ /// MarkNode and the hashtags as @lexical/hashtag TextNode runs. With
1157
+ /// no rules, the mark's wrapper is lost but "rocks" survives, and
1158
+ /// hashtags render as their text.
1159
+ #[test]
1160
+ fn unknown_inline_wrappers_keep_their_text() {
1161
+ let bytes = include_bytes!("fixtures/mark_hashtag.bin");
1162
+ let doc = Doc::new();
1163
+ doc.transact_mut()
1164
+ .apply_update(Update::decode_v1(bytes).unwrap())
1165
+ .unwrap();
1166
+ let txn = doc.transact();
1167
+ let frag = txn.get_xml_fragment("root").unwrap();
1168
+ let html = render(&txn, &frag).unwrap();
1169
+
1170
+ assert!(html.contains("rocks"), "marked text survives without a rule: {html}");
1171
+ assert!(!html.contains("<mark"), "no wrapper is invented for it: {html}");
1172
+ assert!(html.contains("#ruby"), "hashtag text survives: {html}");
1173
+ }
1174
+
1143
1175
  #[test]
1144
1176
  fn format_runs_match_lexxys_export_algorithm() {
1145
1177
  // Singles take their semantic tag; the span for plain text unwraps.
@@ -3,7 +3,7 @@ name = "prosemirror-yjs-html"
3
3
  description = "Render ProseMirror-shaped yrs documents to HTML, no browser or Node required"
4
4
  repository = "https://github.com/jpcamara/yrby"
5
5
  readme = "README.md"
6
- version = "0.1.4"
6
+ version = "0.1.5"
7
7
  edition = "2024"
8
8
  rust-version = "1.85"
9
9
  authors = ["JP Camara <johnpcamara@gmail.com>"]
@@ -881,14 +881,18 @@ fn num_attr<T: ReadTxn>(txn: &T, e: &XmlElementRef, name: &str) -> Option<i64> {
881
881
  }
882
882
 
883
883
  /// Text-content escaping, matching the browser serializer: `&`, `<`, `>`.
884
- fn escape_text(s: &str) -> String {
884
+ /// Public for splice callers. Values read from a deferred segment's
885
+ /// attributes come from collaborators, so escape anything you put into
886
+ /// markup.
887
+ pub fn escape_text(s: &str) -> String {
885
888
  s.replace('&', "&amp;")
886
889
  .replace('<', "&lt;")
887
890
  .replace('>', "&gt;")
888
891
  }
889
892
 
890
- /// Attribute-value escaping: text escaping plus `"`.
891
- fn escape_attr(s: &str) -> String {
893
+ /// Attribute-value escaping: text escaping plus `"`. Public for splice
894
+ /// callers, for document values interpolated into attribute positions.
895
+ pub fn escape_attr(s: &str) -> String {
892
896
  escape_text(s).replace('"', "&quot;")
893
897
  }
894
898
 
data/lib/y/lexxy.rb CHANGED
@@ -42,8 +42,8 @@ module Y
42
42
  def self.list_item(node)
43
43
  out = +"<li"
44
44
  checked = node.attrs["__checked"]
45
- out << %( aria-checked="#{checked}") unless checked.nil?
46
- out << %( value="#{node.attrs["__value"] || 1}")
45
+ out << %( aria-checked="#{RenderRules.escape_attr(checked)}") unless checked.nil?
46
+ out << %( value="#{RenderRules.escape_attr(node.attrs["__value"] || 1)}")
47
47
  out << %( class="lexxy-nested-listitem") if node.child_types.include?("list")
48
48
  "#{out}>#{node.content}</li>"
49
49
  end
data/lib/y/rendering.rb CHANGED
@@ -241,6 +241,18 @@ module Y
241
241
  json = root.nil? ? @native.node_types : @native.node_types(root)
242
242
  json && JSON.parse(json)
243
243
  end
244
+
245
+ # The node types to_html can't fully render: types in the document that
246
+ # neither the built-in schema nor a registered rule handles. An unknown
247
+ # container or inline wrapper renders its children without its own
248
+ # markup, so its text stays. An unknown decorator renders nothing, so
249
+ # content kept only in its attributes is missing from the HTML. Returns
250
+ # [] when every type is handled, and when the root is missing or isn't
251
+ # Lexical-shaped, because to_html returns nil there.
252
+ def unknown_types(root = nil)
253
+ types = node_types(root)
254
+ types ? types.filter_map { |type, info| type if info["handled"].nil? } : []
255
+ end
244
256
  end
245
257
 
246
258
  class ProseMirror
@@ -276,6 +288,18 @@ module Y
276
288
  json = root.nil? ? @native.node_types : @native.node_types(root)
277
289
  json && JSON.parse(json)
278
290
  end
291
+
292
+ # The node types to_html can't fully render: types in the document that
293
+ # neither the built-in schema nor a registered rule handles. An unknown
294
+ # node renders its text and children without its own markup. A node that
295
+ # keeps its content only in its attributes renders nothing. Marks aren't
296
+ # listed, and an unknown mark renders its text unformatted. Returns []
297
+ # when every type is handled, and when the root is missing or isn't
298
+ # ProseMirror-shaped, because to_html returns nil there.
299
+ def unknown_types(root = nil)
300
+ types = node_types(root)
301
+ types ? types.filter_map { |type, info| type if info["handled"].nil? } : []
302
+ end
279
303
  end
280
304
 
281
305
  private_constant :NativeLexical, :NativeProseMirror
data/lib/y/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Y
4
- VERSION = "0.8.0"
4
+ VERSION = "0.8.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yrby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - JP Camara