editor bugfixes, RTL text support

Orual f51cda4a fb1645bc

+1164 -730
+1
Cargo.lock
··· 11846 "tokio", 11847 "tokio-util", 11848 "tracing", 11849 "unicode-normalization", 11850 "url", 11851 "weaver-api",
··· 11846 "tokio", 11847 "tokio-util", 11848 "tracing", 11849 + "unicode-bidi", 11850 "unicode-normalization", 11851 "url", 11852 "weaver-api",
+3 -3
crates/weaver-app/assets/styling/cards-base.css
··· 95 font-size: 0.875rem; 96 font-weight: 600; 97 margin-top: 0; 98 - margin-left: 0; 99 - margin-right: 0; 100 } 101 102 .card-preview p { ··· 122 ========================================================================== */ 123 124 .card-hover-border:hover { 125 - border-left-color: var(--color-secondary); 126 } 127 128 .card-hover-title:hover .card-title {
··· 95 font-size: 0.875rem; 96 font-weight: 600; 97 margin-top: 0; 98 + margin-inline-start: 0; 99 + margin-inline-end: 0; 100 } 101 102 .card-preview p { ··· 122 ========================================================================== */ 123 124 .card-hover-border:hover { 125 + border-inline-start-color: var(--color-secondary); 126 } 127 128 .card-hover-title:hover .card-title {
+48 -16
crates/weaver-app/assets/styling/editor.css
··· 159 background: var(--color-base); 160 border: 1px solid var(--color-overlay); 161 color: var(--color-text); 162 } 163 164 .editor-content:focus { ··· 201 202 .editor-debug { 203 padding: 8px; 204 - padding-right: 0; 205 background: var(--color-base); 206 font-family: var(--font-mono); 207 font-size: 12px; ··· 296 297 /* Editor page header with report button */ 298 .editor-header { 299 - padding-left: 6rem; 300 background: var(--color-base); 301 } 302 ··· 323 324 .report-dialog-overlay { 325 position: fixed; 326 top: 0; 327 - left: 0; 328 - right: 0; 329 bottom: 0; 330 background: rgba(0, 0, 0, 0.6); 331 display: flex; ··· 440 display: flex; 441 align-items: center; 442 gap: 12px; 443 - margin-left: auto; 444 flex-shrink: 0; 445 } 446 ··· 466 467 .publish-dialog-overlay { 468 position: fixed; 469 top: 0; 470 - left: 0; 471 - right: 0; 472 bottom: 0; 473 background: rgba(0, 0, 0, 0.6); 474 display: flex; ··· 795 font-weight: 500; 796 font-family: var(--font-mono); 797 text-transform: uppercase; 798 - margin-left: -6px; 799 position: relative; 800 overflow: hidden; 801 } ··· 808 } 809 810 .collab-avatar:first-child { 811 - margin-left: 0; 812 } 813 814 .collab-avatar.collab-overflow { ··· 832 /* Collaborators panel overlay */ 833 .collaborators-overlay { 834 position: fixed; 835 top: 0; 836 - left: 0; 837 - right: 0; 838 bottom: 0; 839 background: rgba(0, 0, 0, 0.4); 840 display: flex; ··· 953 954 .invite-dialog-overlay { 955 position: fixed; 956 top: 0; 957 - left: 0; 958 - right: 0; 959 bottom: 0; 960 background: rgba(0, 0, 0, 0.4); 961 display: flex; ··· 1123 margin: 6px 0 0 0; 1124 padding: 8px; 1125 background: var(--color-overlay); 1126 - border-left: 2px solid var(--color-border); 1127 } 1128 1129 .invite-actions, ··· 1174 1175 .remote-cursors-overlay { 1176 position: absolute; 1177 top: 0; 1178 - left: 0; 1179 - right: 0; 1180 bottom: 0; 1181 pointer-events: none; 1182 z-index: 10; ··· 1226 opacity: 0.5; 1227 } 1228 }
··· 159 background: var(--color-base); 160 border: 1px solid var(--color-overlay); 161 color: var(--color-text); 162 + /* break-spaces ensures trailing whitespace takes up space and allows cursor placement */ 163 + white-space-collapse: break-spaces; 164 } 165 166 .editor-content:focus { ··· 203 204 .editor-debug { 205 padding: 8px; 206 + padding-inline-end: 0; 207 background: var(--color-base); 208 font-family: var(--font-mono); 209 font-size: 12px; ··· 298 299 /* Editor page header with report button */ 300 .editor-header { 301 + padding-inline-start: 6rem; 302 background: var(--color-base); 303 } 304 ··· 325 326 .report-dialog-overlay { 327 position: fixed; 328 + inset-inline: 0; 329 top: 0; 330 bottom: 0; 331 background: rgba(0, 0, 0, 0.6); 332 display: flex; ··· 441 display: flex; 442 align-items: center; 443 gap: 12px; 444 + margin-inline-start: auto; 445 flex-shrink: 0; 446 } 447 ··· 467 468 .publish-dialog-overlay { 469 position: fixed; 470 + inset-inline: 0; 471 top: 0; 472 bottom: 0; 473 background: rgba(0, 0, 0, 0.6); 474 display: flex; ··· 795 font-weight: 500; 796 font-family: var(--font-mono); 797 text-transform: uppercase; 798 + margin-inline-start: -6px; 799 position: relative; 800 overflow: hidden; 801 } ··· 808 } 809 810 .collab-avatar:first-child { 811 + margin-inline-start: 0; 812 } 813 814 .collab-avatar.collab-overflow { ··· 832 /* Collaborators panel overlay */ 833 .collaborators-overlay { 834 position: fixed; 835 + inset-inline: 0; 836 top: 0; 837 bottom: 0; 838 background: rgba(0, 0, 0, 0.4); 839 display: flex; ··· 952 953 .invite-dialog-overlay { 954 position: fixed; 955 + inset-inline: 0; 956 top: 0; 957 bottom: 0; 958 background: rgba(0, 0, 0, 0.4); 959 display: flex; ··· 1121 margin: 6px 0 0 0; 1122 padding: 8px; 1123 background: var(--color-overlay); 1124 + border-inline-start: 2px solid var(--color-border); 1125 } 1126 1127 .invite-actions, ··· 1172 1173 .remote-cursors-overlay { 1174 position: absolute; 1175 + inset-inline: 0; 1176 top: 0; 1177 bottom: 0; 1178 pointer-events: none; 1179 z-index: 10; ··· 1223 opacity: 0.5; 1224 } 1225 } 1226 + 1227 + /* ========================================================================== 1228 + Footnotes (Editor Mode) - styled but visible, no reordering 1229 + ========================================================================== */ 1230 + 1231 + /* Inline footnote reference [^name] */ 1232 + .footnote-ref { 1233 + font-family: var(--font-mono); 1234 + font-size: 0.9rem; 1235 + color: var(--color-subtle); 1236 + background: color-mix(in srgb, var(--color-surface) 10%, transparent); 1237 + padding: 0 2px; 1238 + } 1239 + 1240 + /* Footnote definition wrapper - stays in place, no reordering */ 1241 + .footnote-def-editor { 1242 + margin-bottom: 1rem; 1243 + margin-top: -1rem; 1244 + margin-inline-start: 2rem; 1245 + padding: 0 0.5rem; 1246 + border-inline-start: 2px solid var(--color-border); 1247 + } 1248 + 1249 + /* The [^name]: prefix in definitions */ 1250 + .footnote-def-syntax { 1251 + font-family: var(--font-mono); 1252 + font-size: 0.9rem; 1253 + color: var(--color-subtle); 1254 + } 1255 + 1256 + /* Inner paragraphs in footnote defs */ 1257 + .footnote-def-editor p { 1258 + margin: 0; 1259 + display: inline; 1260 + }
+1 -1
crates/weaver-app/assets/styling/entry-actions.css
··· 53 display: block; 54 width: 100%; 55 padding: 0.5rem 0.75rem; 56 - text-align: left; 57 background: none; 58 border: none; 59 cursor: pointer;
··· 53 display: block; 54 width: 100%; 55 padding: 0.5rem 0.75rem; 56 + text-align: start; 57 background: none; 58 border: none; 59 cursor: pointer;
+12 -12
crates/weaver-app/assets/styling/entry-card.css
··· 12 display: block; 13 background: var(--color-surface); 14 box-shadow: 0 1px 2px color-mix(in srgb, var(--color-text) 6%, transparent); 15 - border-left: 2px solid transparent; 16 padding: 1.25rem; 17 text-decoration: none; 18 color: var(--color-text); ··· 23 24 .entry-card-link:hover { 25 box-shadow: 0 2px 4px color-mix(in srgb, var(--color-text) 10%, transparent); 26 - border-left-color: var(--color-secondary); 27 } 28 29 .entry-card-link:hover .entry-card-title { ··· 83 } 84 85 .feed-entry-card:hover { 86 - border-left: 2px solid var(--color-secondary); 87 - margin-left: -1px; 88 } 89 90 .feed-entry-card:hover .entry-card-title { ··· 103 } 104 105 .feed-entry-card .entry-card-byline .entry-card-date { 106 - margin-left: auto; 107 } 108 109 /* Date in header (no author) aligns right */ 110 .feed-entry-card .entry-card-header .entry-card-date { 111 - margin-left: auto; 112 } 113 114 .entry-card-stats { ··· 121 122 .entry-card-stats .word-count::after { 123 content: "·"; 124 - margin-left: 0.5rem; 125 } 126 127 .entry-card-tags { ··· 178 font-size: 0.875rem; 179 font-weight: 600; 180 margin-top: 0.5rem; 181 - margin-left: 0; 182 - margin-right: 0; 183 } 184 185 .entry-card-preview p { ··· 201 .feed-entry-card { 202 box-shadow: none; 203 border-top: 1px solid var(--color-border); 204 - border-right: 1px solid var(--color-border); 205 border-bottom: 1px solid var(--color-border); 206 207 - border-left: 1px solid var(--color-border); 208 - /* Keep border-left as accent */ 209 } 210 }
··· 12 display: block; 13 background: var(--color-surface); 14 box-shadow: 0 1px 2px color-mix(in srgb, var(--color-text) 6%, transparent); 15 + border-inline-start: 2px solid transparent; 16 padding: 1.25rem; 17 text-decoration: none; 18 color: var(--color-text); ··· 23 24 .entry-card-link:hover { 25 box-shadow: 0 2px 4px color-mix(in srgb, var(--color-text) 10%, transparent); 26 + border-inline-start-color: var(--color-secondary); 27 } 28 29 .entry-card-link:hover .entry-card-title { ··· 83 } 84 85 .feed-entry-card:hover { 86 + border-inline-start: 2px solid var(--color-secondary); 87 + margin-inline-start: -1px; 88 } 89 90 .feed-entry-card:hover .entry-card-title { ··· 103 } 104 105 .feed-entry-card .entry-card-byline .entry-card-date { 106 + margin-inline-start: auto; 107 } 108 109 /* Date in header (no author) aligns right */ 110 .feed-entry-card .entry-card-header .entry-card-date { 111 + margin-inline-start: auto; 112 } 113 114 .entry-card-stats { ··· 121 122 .entry-card-stats .word-count::after { 123 content: "·"; 124 + margin-inline-start: 0.5rem; 125 } 126 127 .entry-card-tags { ··· 178 font-size: 0.875rem; 179 font-weight: 600; 180 margin-top: 0.5rem; 181 + margin-inline-start: 0; 182 + margin-inline-end: 0; 183 } 184 185 .entry-card-preview p { ··· 201 .feed-entry-card { 202 box-shadow: none; 203 border-top: 1px solid var(--color-border); 204 + border-inline-end: 1px solid var(--color-border); 205 border-bottom: 1px solid var(--color-border); 206 207 + border-inline-start: 1px solid var(--color-border); 208 + /* Keep border-inline-start as accent */ 209 } 210 }
+8 -8
crates/weaver-app/assets/styling/entry.css
··· 104 105 .entry-footer-nav .nav-button-prev { 106 align-items: flex-start; 107 - text-align: left; 108 } 109 110 .entry-footer-nav .nav-button-next { 111 align-items: flex-end; 112 - text-align: right; 113 - margin-left: auto; 114 } 115 116 .entry-footer-nav .nav-arrow { ··· 176 177 .entry-reading-stats { 178 color: var(--color-subtle); 179 - margin-left: auto; 180 margin-top: 0.25rem; 181 } 182 183 .entry-reading-stats .word-count::after { 184 content: "·"; 185 - margin-left: 0.5rem; 186 } 187 188 .entry-date { 189 - margin-left: auto; 190 font-weight: 400; 191 align-items: last baseline; 192 color: var(--color-subtle); ··· 255 } 256 257 .entry-content-main blockquote { 258 - border-left-color: var(--color-secondary); 259 background: var(--color-surface); 260 } 261 ··· 292 293 .entry-header .nav-button-next { 294 order: 1; 295 - margin-left: auto; 296 } 297 } 298
··· 104 105 .entry-footer-nav .nav-button-prev { 106 align-items: flex-start; 107 + text-align: start; 108 } 109 110 .entry-footer-nav .nav-button-next { 111 align-items: flex-end; 112 + text-align: end; 113 + margin-inline-start: auto; 114 } 115 116 .entry-footer-nav .nav-arrow { ··· 176 177 .entry-reading-stats { 178 color: var(--color-subtle); 179 + margin-inline-start: auto; 180 margin-top: 0.25rem; 181 } 182 183 .entry-reading-stats .word-count::after { 184 content: "·"; 185 + margin-inline-start: 0.5rem; 186 } 187 188 .entry-date { 189 + margin-inline-start: auto; 190 font-weight: 400; 191 align-items: last baseline; 192 color: var(--color-subtle); ··· 255 } 256 257 .entry-content-main blockquote { 258 + border-inline-start-color: var(--color-secondary); 259 background: var(--color-surface); 260 } 261 ··· 292 293 .entry-header .nav-button-next { 294 order: 1; 295 + margin-inline-start: auto; 296 } 297 } 298
+3 -3
crates/weaver-app/assets/styling/home.css
··· 25 .pinned-items .entry-card, 26 .pinned-items .feed-entry-card, 27 .pinned-items .notebook-card { 28 - border-left: 3px solid var(--color-primary) !important; 29 } 30 31 .pinned-items .feed-entry-card:hover { 32 - border-left: 3px solid var(--color-secondary) !important; 33 - margin-left: 0; 34 } 35 36 .feed-section {
··· 25 .pinned-items .entry-card, 26 .pinned-items .feed-entry-card, 27 .pinned-items .notebook-card { 28 + border-inline-start: 3px solid var(--color-primary) !important; 29 } 30 31 .pinned-items .feed-entry-card:hover { 32 + border-inline-start: 3px solid var(--color-secondary) !important; 33 + margin-inline-start: 0; 34 } 35 36 .feed-section {
+1 -1
crates/weaver-app/assets/styling/invites.css
··· 72 margin: 0.5rem 0 0 0; 73 padding: 0.5rem; 74 background: var(--color-background); 75 - border-left: 3px solid var(--color-primary); 76 font-style: italic; 77 } 78
··· 72 margin: 0.5rem 0 0 0; 73 padding: 0.5rem; 74 background: var(--color-background); 75 + border-inline-start: 3px solid var(--color-primary); 76 font-style: italic; 77 } 78
+4 -4
crates/weaver-app/assets/styling/navbar.css
··· 2 display: flex; 3 flex-direction: row; 4 justify-content: space-between; 5 - padding-left: 1rem; 6 padding-top: 1rem; 7 - padding-right: 1rem; 8 } 9 10 .breadcrumbs { ··· 48 display: flex; 49 align-items: center; 50 gap: 1rem; 51 - margin-left: auto; 52 - margin-right: 1rem; 53 } 54 55 .nav-tool-link {
··· 2 display: flex; 3 flex-direction: row; 4 justify-content: space-between; 5 + padding-inline-start: 1rem; 6 padding-top: 1rem; 7 + padding-inline-end: 1rem; 8 } 9 10 .breadcrumbs { ··· 48 display: flex; 49 align-items: center; 50 gap: 1rem; 51 + margin-inline-start: auto; 52 + margin-inline-end: 1rem; 53 } 54 55 .nav-tool-link {
+12 -12
crates/weaver-app/assets/styling/notebook-card.css
··· 19 display: block; 20 text-decoration: none; 21 color: var(--color-text); 22 - border-left: 3px solid transparent; 23 - padding-left: 0.75rem; 24 - margin-left: -0.75rem; 25 transition: border-color 0.2s ease; 26 } 27 28 .notebook-card-header-link:hover { 29 - border-left-color: var(--color-primary); 30 } 31 32 .notebook-card-header-link:hover .notebook-card-title { ··· 130 .notebook-card-date { 131 color: var(--color-muted); 132 font-size: 0.85rem; 133 - margin-left: 0.25rem; 134 } 135 136 .notebook-card-authors { ··· 157 display: block; 158 text-decoration: none; 159 color: var(--color-text); 160 - border-left: 2px solid transparent; 161 border-top: 1px solid var(--color-border); 162 - padding-left: 0.625rem; /* 0.5 grid */ 163 padding-top: 1.25rem; 164 - margin-left: -0.625rem; 165 - transition: border-left-color 0.2s ease; 166 } 167 168 .notebook-entry-preview-link:first-child { ··· 171 } 172 173 .notebook-entry-preview-link:hover { 174 - border-left-color: var(--color-secondary); 175 } 176 177 .notebook-entry-preview-link:hover .entry-preview-title { ··· 255 font-size: 0.875rem; 256 font-weight: 600; 257 margin-top: 0; 258 - margin-left: 0; 259 - margin-right: 0; 260 } 261 262 .entry-preview-content code {
··· 19 display: block; 20 text-decoration: none; 21 color: var(--color-text); 22 + border-inline-start: 3px solid transparent; 23 + padding-inline-start: 0.75rem; 24 + margin-inline-start: -0.75rem; 25 transition: border-color 0.2s ease; 26 } 27 28 .notebook-card-header-link:hover { 29 + border-inline-start-color: var(--color-primary); 30 } 31 32 .notebook-card-header-link:hover .notebook-card-title { ··· 130 .notebook-card-date { 131 color: var(--color-muted); 132 font-size: 0.85rem; 133 + margin-inline-start: 0.25rem; 134 } 135 136 .notebook-card-authors { ··· 157 display: block; 158 text-decoration: none; 159 color: var(--color-text); 160 + border-inline-start: 2px solid transparent; 161 border-top: 1px solid var(--color-border); 162 + padding-inline-start: 0.625rem; /* 0.5 grid */ 163 padding-top: 1.25rem; 164 + margin-inline-start: -0.625rem; 165 + transition: border-inline-start-color 0.2s ease; 166 } 167 168 .notebook-entry-preview-link:first-child { ··· 171 } 172 173 .notebook-entry-preview-link:hover { 174 + border-inline-start-color: var(--color-secondary); 175 } 176 177 .notebook-entry-preview-link:hover .entry-preview-title { ··· 255 font-size: 0.875rem; 256 font-weight: 600; 257 margin-top: 0; 258 + margin-inline-start: 0; 259 + margin-inline-end: 0; 260 } 261 262 .entry-preview-content code {
+2 -2
crates/weaver-app/assets/styling/notebook-cover.css
··· 8 @media (min-width: 1400px) { 9 .notebook-cover { 10 border-top: 1px solid var(--color-border); 11 - border-right: 1px solid var(--color-border); 12 border-bottom: 1px solid var(--color-border); 13 padding: 1.25rem; 14 } ··· 105 106 .notebook-cover-actions .button { 107 width: 100%; 108 - text-align: left; 109 border-radius: 0; 110 }
··· 8 @media (min-width: 1400px) { 9 .notebook-cover { 10 border-top: 1px solid var(--color-border); 11 + border-inline-end: 1px solid var(--color-border); 12 border-bottom: 1px solid var(--color-border); 13 padding: 1.25rem; 14 } ··· 105 106 .notebook-cover-actions .button { 107 width: 100%; 108 + text-align: start; 109 border-radius: 0; 110 }
+2 -2
crates/weaver-app/assets/styling/profile-actions.css
··· 7 8 .profile-actions-container { 9 padding-top: 2.25rem; 10 - padding-right: 5rem; 11 } 12 13 .profile-actions-title { ··· 33 /* Match notebook-add-entry styling */ 34 .profile-actions-list .button { 35 width: 100%; 36 - text-align: left; 37 font-size: 0.85rem; 38 color: var(--color-primary); 39 background: transparent;
··· 7 8 .profile-actions-container { 9 padding-top: 2.25rem; 10 + padding-inline-end: 5rem; 11 } 12 13 .profile-actions-title { ··· 33 /* Match notebook-add-entry styling */ 34 .profile-actions-list .button { 35 width: 100%; 36 + text-align: start; 37 font-size: 0.85rem; 38 color: var(--color-primary); 39 background: transparent;
+4 -4
crates/weaver-app/assets/styling/profile.css
··· 153 } 154 155 .profile-extras { 156 - padding-left: 1rem; 157 - border-left: 1.5px dashed var(--color-border); 158 } 159 160 .profile-block { ··· 175 } 176 177 .profile-name-section { 178 - margin-left: 1rem; 179 margin-top: -0.5rem; 180 } 181 } ··· 191 @media (min-width: 1400px) { 192 .profile-display { 193 border-top: 1.5px solid var(--color-border); 194 - border-right: 1.5px solid var(--color-border); 195 } 196 } 197
··· 153 } 154 155 .profile-extras { 156 + padding-inline-start: 1rem; 157 + border-inline-start: 1.5px dashed var(--color-border); 158 } 159 160 .profile-block { ··· 175 } 176 177 .profile-name-section { 178 + margin-inline-start: 1rem; 179 margin-top: -0.5rem; 180 } 181 } ··· 191 @media (min-width: 1400px) { 192 .profile-display { 193 border-top: 1.5px solid var(--color-border); 194 + border-inline-end: 1.5px solid var(--color-border); 195 } 196 } 197
+44 -44
crates/weaver-app/assets/styling/record-view.css
··· 73 .metadata-label::after { 74 content: ":"; 75 font-size: 0.8rem; 76 - margin-left: 0.25rem; 77 } 78 79 .metadata-value { ··· 140 } 141 142 .tab-button.edit-button { 143 - margin-left: auto; 144 } 145 146 .action-buttons-group { 147 - margin-left: auto; 148 display: flex; 149 gap: 0; 150 align-items: center; ··· 182 padding: 0.5rem 1rem; 183 background: transparent; 184 border: none; 185 - text-align: left; 186 cursor: pointer; 187 color: var(--color-text); 188 font-family: var(--font-mono); ··· 200 display: flex; 201 flex-direction: column; 202 align-items: flex-start; 203 - border-left: 1px dashed var(--color-subtle); 204 } 205 206 .record-field { 207 display: flex; 208 flex-direction: column; 209 padding: 0rem 0 0rem 1rem; 210 - padding-right: 1rem; 211 - margin-left: -1px; 212 - border-left: 2px solid var(--color-secondary); 213 border-bottom: 1px dashed var(--color-subtle); 214 z-index: 1; 215 } 216 217 .record-field.field-error { 218 - border-left-color: var(--color-error); 219 background-color: rgba(255, 107, 107, 0.05); 220 } 221 ··· 224 font-size: 0.85rem; 225 color: var(--color-error); 226 padding: 0.25rem 0; 227 - border-left: 2px solid var(--color-error); 228 border-bottom: 1px dashed var(--color-error); 229 - padding-left: 0.5rem; 230 word-wrap: break-word; 231 - margin-left: -1px; 232 word-break: break-word; 233 overflow-wrap: break-word; 234 } ··· 254 color: var(--color-subtle); 255 font-size: 0.9rem; 256 font-weight: 400; 257 - padding-left: 0.125rem; 258 padding-top: 0.5rem; 259 } 260 ··· 285 286 .record-section { 287 position: relative; 288 - margin-left: 1.5rem; 289 - border-left: 1px dashed var(--color-border); 290 } 291 292 .section-content .record-section { 293 position: relative; 294 - border-left: 1px dashed var(--color-border); 295 } 296 297 .array-item .record-section { 298 position: relative; 299 - border-left: 1px dashed var(--color-border); 300 } 301 302 .section-label { ··· 304 color: var(--color-primary); 305 font-size: 1rem; 306 font-weight: 600; 307 - padding-left: 1rem; 308 padding-top: 0.5rem; 309 padding-bottom: 0.25rem; 310 - margin-left: -1px; 311 - border-left: 2px solid var(--color-primary); 312 border-bottom: 1px dashed var(--color-muted); 313 } 314 ··· 317 color: var(--color-tertiary); 318 font-size: 0.9rem; 319 font-weight: 600; 320 - padding-left: 1rem; 321 padding-top: 0.5rem; 322 - margin-left: -1px; 323 - border-left: 2px solid var(--color-secondary); 324 } 325 326 .section-content { 327 display: flex; 328 flex-direction: column; 329 align-items: flex-start; 330 - padding-right: 1rem; 331 } 332 333 .section-content .record-field { 334 - border-left-color: var(--color-secondary); 335 opacity: 0.95; 336 align-self: stretch; 337 width: 100%; ··· 536 537 .validation-errors .error { 538 padding: 0.25rem 0; 539 - border-left: 2px solid var(--color-error); 540 - padding-left: 0.5rem; 541 margin: 0.25rem 0; 542 word-wrap: break-word; 543 word-break: break-word; ··· 574 width: 100%; 575 display: flex; 576 flex-direction: column; 577 - margin-left: 1.48rem; 578 } 579 580 /* Pretty Editor Input Fields */ ··· 627 .record-field textarea::-webkit-resizer { 628 background: transparent; 629 border: 2px solid var(--color-border); 630 - border-left: none; 631 border-bottom: none; 632 border-top: none; 633 } ··· 657 background: var(--color-surface); 658 border: 1px solid var(--color-border); 659 padding: 0.25rem 0.25rem; 660 - margin-right: 0.5rem; 661 margin-bottom: 0.2rem; 662 cursor: pointer; 663 transition: ··· 694 align-items: center; 695 padding: 0.75rem 0 0rem 0rem; 696 margin-bottom: 1rem; 697 - margin-left: 1.5rem; 698 - border-left: 1px solid var(--color-border); 699 } 700 701 .add-field-widget input[type="text"] { ··· 705 background: var(--color-background-alt, rgba(0, 0, 0, 0.2)); 706 border: 1px solid var(--color-border); 707 padding: 0.3rem 0.5rem; 708 - margin-left: -1px; 709 outline: none; 710 } 711 ··· 719 text-transform: uppercase; 720 letter-spacing: 0.05em; 721 padding: 0.3rem 0.75rem; 722 - margin-left: -1px; 723 background: transparent; 724 border: 1px solid var(--color-border); 725 color: var(--color-primary); ··· 733 text-transform: uppercase; 734 letter-spacing: 0.05em; 735 padding: 0.36rem 0.75rem; 736 - margin-left: -1px; 737 background: transparent; 738 border: 1px solid var(--color-primary); 739 color: var(--color-primary); ··· 799 background: var(--color-surface); 800 border: 1px dashed var(--color-border); 801 padding: 0.25rem 0.5rem; 802 - margin-right: 0.5rem; 803 margin-bottom: -0.2rem; 804 cursor: pointer; 805 transition: ··· 933 934 .accordion-content { 935 grid-template-rows: 1fr; 936 - padding-left: 46px; 937 } 938 939 .accordion-content .section-content { 940 - margin-left: -1px; 941 } 942 943 .accordion-content .record-field { 944 - margin-left: 0px; 945 } 946 947 .accordion-content .array-item { 948 - margin-left: 0px; 949 } 950 951 .accordion-content .array-item .section-content { ··· 953 } 954 955 .accordion-content .array-item .record-section { 956 - margin-left: 1.5rem; 957 } 958 959 .accordion-content .array-item .record-section .record-field { 960 - margin-left: -1px; 961 } 962 963 .accordion-content .array-item .record-section .accordion-trigger .section-label { 964 - margin-left: -2px; 965 } 966 967 .accordion-trigger { ··· 969 } 970 971 .accordion { 972 - margin-left: -46px; 973 }
··· 73 .metadata-label::after { 74 content: ":"; 75 font-size: 0.8rem; 76 + margin-inline-start: 0.25rem; 77 } 78 79 .metadata-value { ··· 140 } 141 142 .tab-button.edit-button { 143 + margin-inline-start: auto; 144 } 145 146 .action-buttons-group { 147 + margin-inline-start: auto; 148 display: flex; 149 gap: 0; 150 align-items: center; ··· 182 padding: 0.5rem 1rem; 183 background: transparent; 184 border: none; 185 + text-align: start; 186 cursor: pointer; 187 color: var(--color-text); 188 font-family: var(--font-mono); ··· 200 display: flex; 201 flex-direction: column; 202 align-items: flex-start; 203 + border-inline-start: 1px dashed var(--color-subtle); 204 } 205 206 .record-field { 207 display: flex; 208 flex-direction: column; 209 padding: 0rem 0 0rem 1rem; 210 + padding-inline-end: 1rem; 211 + margin-inline-start: -1px; 212 + border-inline-start: 2px solid var(--color-secondary); 213 border-bottom: 1px dashed var(--color-subtle); 214 z-index: 1; 215 } 216 217 .record-field.field-error { 218 + border-inline-start-color: var(--color-error); 219 background-color: rgba(255, 107, 107, 0.05); 220 } 221 ··· 224 font-size: 0.85rem; 225 color: var(--color-error); 226 padding: 0.25rem 0; 227 + border-inline-start: 2px solid var(--color-error); 228 border-bottom: 1px dashed var(--color-error); 229 + padding-inline-start: 0.5rem; 230 word-wrap: break-word; 231 + margin-inline-start: -1px; 232 word-break: break-word; 233 overflow-wrap: break-word; 234 } ··· 254 color: var(--color-subtle); 255 font-size: 0.9rem; 256 font-weight: 400; 257 + padding-inline-start: 0.125rem; 258 padding-top: 0.5rem; 259 } 260 ··· 285 286 .record-section { 287 position: relative; 288 + margin-inline-start: 1.5rem; 289 + border-inline-start: 1px dashed var(--color-border); 290 } 291 292 .section-content .record-section { 293 position: relative; 294 + border-inline-start: 1px dashed var(--color-border); 295 } 296 297 .array-item .record-section { 298 position: relative; 299 + border-inline-start: 1px dashed var(--color-border); 300 } 301 302 .section-label { ··· 304 color: var(--color-primary); 305 font-size: 1rem; 306 font-weight: 600; 307 + padding-inline-start: 1rem; 308 padding-top: 0.5rem; 309 padding-bottom: 0.25rem; 310 + margin-inline-start: -1px; 311 + border-inline-start: 2px solid var(--color-primary); 312 border-bottom: 1px dashed var(--color-muted); 313 } 314 ··· 317 color: var(--color-tertiary); 318 font-size: 0.9rem; 319 font-weight: 600; 320 + padding-inline-start: 1rem; 321 padding-top: 0.5rem; 322 + margin-inline-start: -1px; 323 + border-inline-start: 2px solid var(--color-secondary); 324 } 325 326 .section-content { 327 display: flex; 328 flex-direction: column; 329 align-items: flex-start; 330 + padding-inline-end: 1rem; 331 } 332 333 .section-content .record-field { 334 + border-inline-start-color: var(--color-secondary); 335 opacity: 0.95; 336 align-self: stretch; 337 width: 100%; ··· 536 537 .validation-errors .error { 538 padding: 0.25rem 0; 539 + border-inline-start: 2px solid var(--color-error); 540 + padding-inline-start: 0.5rem; 541 margin: 0.25rem 0; 542 word-wrap: break-word; 543 word-break: break-word; ··· 574 width: 100%; 575 display: flex; 576 flex-direction: column; 577 + margin-inline-start: 1.48rem; 578 } 579 580 /* Pretty Editor Input Fields */ ··· 627 .record-field textarea::-webkit-resizer { 628 background: transparent; 629 border: 2px solid var(--color-border); 630 + border-inline-start: none; 631 border-bottom: none; 632 border-top: none; 633 } ··· 657 background: var(--color-surface); 658 border: 1px solid var(--color-border); 659 padding: 0.25rem 0.25rem; 660 + margin-inline-end: 0.5rem; 661 margin-bottom: 0.2rem; 662 cursor: pointer; 663 transition: ··· 694 align-items: center; 695 padding: 0.75rem 0 0rem 0rem; 696 margin-bottom: 1rem; 697 + margin-inline-start: 1.5rem; 698 + border-inline-start: 1px solid var(--color-border); 699 } 700 701 .add-field-widget input[type="text"] { ··· 705 background: var(--color-background-alt, rgba(0, 0, 0, 0.2)); 706 border: 1px solid var(--color-border); 707 padding: 0.3rem 0.5rem; 708 + margin-inline-start: -1px; 709 outline: none; 710 } 711 ··· 719 text-transform: uppercase; 720 letter-spacing: 0.05em; 721 padding: 0.3rem 0.75rem; 722 + margin-inline-start: -1px; 723 background: transparent; 724 border: 1px solid var(--color-border); 725 color: var(--color-primary); ··· 733 text-transform: uppercase; 734 letter-spacing: 0.05em; 735 padding: 0.36rem 0.75rem; 736 + margin-inline-start: -1px; 737 background: transparent; 738 border: 1px solid var(--color-primary); 739 color: var(--color-primary); ··· 799 background: var(--color-surface); 800 border: 1px dashed var(--color-border); 801 padding: 0.25rem 0.5rem; 802 + margin-inline-end: 0.5rem; 803 margin-bottom: -0.2rem; 804 cursor: pointer; 805 transition: ··· 933 934 .accordion-content { 935 grid-template-rows: 1fr; 936 + padding-inline-start: 46px; 937 } 938 939 .accordion-content .section-content { 940 + margin-inline-start: -1px; 941 } 942 943 .accordion-content .record-field { 944 + margin-inline-start: 0px; 945 } 946 947 .accordion-content .array-item { 948 + margin-inline-start: 0px; 949 } 950 951 .accordion-content .array-item .section-content { ··· 953 } 954 955 .accordion-content .array-item .record-section { 956 + margin-inline-start: 1.5rem; 957 } 958 959 .accordion-content .array-item .record-section .record-field { 960 + margin-inline-start: -1px; 961 } 962 963 .accordion-content .array-item .record-section .accordion-trigger .section-label { 964 + margin-inline-start: -2px; 965 } 966 967 .accordion-trigger { ··· 969 } 970 971 .accordion { 972 + margin-inline-start: -46px; 973 }
+42 -15
crates/weaver-app/src/components/editor/beforeinput.rs
··· 288 // === Insertion === 289 InputType::InsertText => { 290 if let Some(text) = ctx.data { 291 - // Simple text insert - update model, let browser handle DOM 292 - // This mirrors the simple delete handling: we track in model, 293 - // browser handles visual update, DOM sync skips innerHTML for 294 - // cursor paragraph when syntax is unchanged 295 let action = EditorAction::Insert { 296 text: text.clone(), 297 range, 298 }; 299 execute_action(doc, &action); 300 - tracing::trace!( 301 - text_len = text.len(), 302 - range_start = range.start, 303 - range_end = range.end, 304 - cursor_after = doc.cursor.read().offset, 305 - "insertText: updated model, PassThrough to browser" 306 - ); 307 - BeforeInputResult::PassThrough 308 } else { 309 BeforeInputResult::PassThrough 310 } ··· 372 }; 373 374 if needs_special_handling { 375 - // Complex delete - we handle everything, prevent browser default 376 let action = EditorAction::DeleteBackward { range }; 377 execute_action(doc, &action); 378 BeforeInputResult::Handled ··· 388 doc.selection.set(None); 389 } 390 tracing::debug!("deleteContentBackward: after model update, returning PassThrough"); 391 - BeforeInputResult::PassThrough 392 } 393 } 394 ··· 404 }; 405 406 if needs_special_handling { 407 let action = EditorAction::DeleteForward { range }; 408 execute_action(doc, &action); 409 BeforeInputResult::Handled ··· 413 let _ = doc.remove_tracked(range.start, 1); 414 doc.selection.set(None); 415 } 416 - BeforeInputResult::PassThrough 417 } 418 } 419
··· 288 // === Insertion === 289 InputType::InsertText => { 290 if let Some(text) = ctx.data { 291 + use super::FORCE_INNERHTML_UPDATE; 292 + 293 let action = EditorAction::Insert { 294 text: text.clone(), 295 range, 296 }; 297 execute_action(doc, &action); 298 + 299 + // Log model content after insert to detect ZWC contamination 300 + if tracing::enabled!(tracing::Level::TRACE) { 301 + let content = doc.content(); 302 + tracing::trace!( 303 + text_len = text.len(), 304 + range_start = range.start, 305 + range_end = range.end, 306 + cursor_after = doc.cursor.read().offset, 307 + model_len = content.len(), 308 + model_chars = content.chars().count(), 309 + model_content = %content.escape_debug(), 310 + force_innerhtml = FORCE_INNERHTML_UPDATE, 311 + "insertText: updated model" 312 + ); 313 + } 314 + 315 + // When FORCE_INNERHTML_UPDATE is true, dom_sync will always replace 316 + // innerHTML. We must preventDefault to avoid browser's default action 317 + // racing with our innerHTML update and causing double-insertion. 318 + if FORCE_INNERHTML_UPDATE { 319 + BeforeInputResult::Handled 320 + } else { 321 + // PassThrough: browser handles DOM, we just track in model. 322 + // dom_sync will skip innerHTML for cursor paragraph when syntax unchanged. 323 + BeforeInputResult::PassThrough 324 + } 325 } else { 326 BeforeInputResult::PassThrough 327 } ··· 389 }; 390 391 if needs_special_handling { 392 + // Handle fully when: complex delete OR when dom_sync will replace innerHTML 393 + // (FORCE_INNERHTML_UPDATE). PassThrough + innerHTML causes double-deletion. 394 let action = EditorAction::DeleteBackward { range }; 395 execute_action(doc, &action); 396 BeforeInputResult::Handled ··· 406 doc.selection.set(None); 407 } 408 tracing::debug!("deleteContentBackward: after model update, returning PassThrough"); 409 + if super::FORCE_INNERHTML_UPDATE { 410 + BeforeInputResult::Handled 411 + } else { 412 + BeforeInputResult::PassThrough 413 + } 414 } 415 } 416 ··· 426 }; 427 428 if needs_special_handling { 429 + // Handle fully when: complex delete OR when dom_sync will replace innerHTML 430 let action = EditorAction::DeleteForward { range }; 431 execute_action(doc, &action); 432 BeforeInputResult::Handled ··· 436 let _ = doc.remove_tracked(range.start, 1); 437 doc.selection.set(None); 438 } 439 + if super::FORCE_INNERHTML_UPDATE { 440 + BeforeInputResult::Handled 441 + } else { 442 + BeforeInputResult::PassThrough 443 + } 444 } 445 } 446
+12 -3
crates/weaver-app/src/components/editor/dom_sync.rs
··· 496 } 497 498 if needs_update { 499 - // TESTING: Force innerHTML update to measure timing cost 500 - // TODO: Remove this flag after benchmarking 501 - const FORCE_INNERHTML_UPDATE: bool = true; 502 503 // For cursor paragraph: only update if syntax/formatting changed 504 // This prevents destroying browser selection during fast typing ··· 549 // Update hash - browser native editing has the correct content 550 let _ = existing_elem.set_attribute("data-hash", &new_hash); 551 } else { 552 // Timing instrumentation for innerHTML update cost 553 let start = web_sys::window() 554 .and_then(|w| w.performance())
··· 496 } 497 498 if needs_update { 499 + use super::FORCE_INNERHTML_UPDATE; 500 501 // For cursor paragraph: only update if syntax/formatting changed 502 // This prevents destroying browser selection during fast typing ··· 547 // Update hash - browser native editing has the correct content 548 let _ = existing_elem.set_attribute("data-hash", &new_hash); 549 } else { 550 + // Log old innerHTML before replacement to see what browser did 551 + if tracing::enabled!(tracing::Level::TRACE) { 552 + let old_inner = existing_elem.inner_html(); 553 + tracing::trace!( 554 + para_id = %para_id, 555 + old_inner = %old_inner.escape_debug(), 556 + new_html = %new_para.html.escape_debug(), 557 + "update_paragraph_dom: replacing innerHTML" 558 + ); 559 + } 560 + 561 // Timing instrumentation for innerHTML update cost 562 let start = web_sys::window() 563 .and_then(|w| w.performance())
+9
crates/weaver-app/src/components/editor/mod.rs
··· 32 #[cfg(test)] 33 mod tests; 34 35 // Main component 36 pub use component::MarkdownEditor; 37
··· 32 #[cfg(test)] 33 mod tests; 34 35 + /// When true, always update innerHTML even for cursor paragraph during typing. 36 + /// This ensures syntax/formatting changes are immediately visible, but requires 37 + /// using `Handled` (preventDefault) for InsertText to avoid double-insertion 38 + /// from browser's default action racing with our innerHTML update. 39 + /// 40 + /// TODO: Replace with granular detection of syntax/formatting changes to allow 41 + /// PassThrough optimization when only text content changes. 42 + pub(crate) const FORCE_INNERHTML_UPDATE: bool = true; 43 + 44 // Main component 45 pub use component::MarkdownEditor; 46
+2
crates/weaver-app/src/components/editor/publish.rs
··· 250 .title(doc.title()) 251 .path(path) 252 .created_at(Datetime::now()) 253 .maybe_tags(tags) 254 .maybe_embeds(entry_embeds) 255 .build(); ··· 400 .title(doc.title()) 401 .path(path) 402 .created_at(Datetime::now()) 403 .maybe_tags(tags) 404 .maybe_embeds(entry_embeds) 405 .build();
··· 250 .title(doc.title()) 251 .path(path) 252 .created_at(Datetime::now()) 253 + .updated_at(Datetime::now()) 254 .maybe_tags(tags) 255 .maybe_embeds(entry_embeds) 256 .build(); ··· 401 .title(doc.title()) 402 .path(path) 403 .created_at(Datetime::now()) 404 + .updated_at(Datetime::now()) 405 .maybe_tags(tags) 406 .maybe_embeds(entry_embeds) 407 .build();
+11
crates/weaver-app/src/components/editor/render.rs
··· 184 let fn_start = crate::perf::now(); 185 let source = text.to_string(); 186 187 // Handle empty document 188 if source.is_empty() { 189 let empty_node_id = "n0".to_string();
··· 184 let fn_start = crate::perf::now(); 185 let source = text.to_string(); 186 187 + // Log source entering renderer to detect ZWC/space issues 188 + if tracing::enabled!(target: "weaver::render", tracing::Level::TRACE) { 189 + tracing::trace!( 190 + target: "weaver::render", 191 + source_len = source.len(), 192 + source_chars = source.chars().count(), 193 + source_content = %source.escape_debug(), 194 + "render_paragraphs: source entering renderer" 195 + ); 196 + } 197 + 198 // Handle empty document 199 if source.is_empty() { 200 let empty_node_id = "n0".to_string();
+23 -13
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__blockquote.snap
··· 8 char_range: 9 - 0 10 - 18 11 - html: "<blockquote>\n<p id=\"n0\"><span class=\"md-syntax-block\" data-syn-id=\"s0\" data-char-start=\"0\" data-char-end=\"1\">&gt;</span> This is a quote\n</p>\n</blockquote>\n" 12 offset_map: 13 - byte_range: 14 - 2 ··· 16 char_range: 17 - 0 18 - 0 19 - node_id: n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 1 29 - node_id: n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 1 ··· 36 char_range: 37 - 1 38 - 2 39 - node_id: n0 40 char_offset_in_node: 1 41 child_index: ~ 42 utf16_len: 1 ··· 46 char_range: 47 - 2 48 - 17 49 - node_id: n0 50 char_offset_in_node: 2 51 child_index: ~ 52 utf16_len: 15 ··· 56 char_range: 57 - 17 58 - 18 59 - node_id: n0 60 char_offset_in_node: 17 61 child_index: ~ 62 utf16_len: 1 ··· 86 char_range: 87 - 22 88 - 41 89 - html: "<p id=\"n1\">With multiple lines</p>\n" 90 offset_map: 91 - byte_range: 92 - - 0 93 - - 0 94 char_range: 95 - 22 96 - 22 97 - node_id: n1 98 char_offset_in_node: 0 99 child_index: 0 100 utf16_len: 0 101 - byte_range: 102 - - 0 103 - - 19 104 char_range: 105 - 22 106 - 41 107 - node_id: n1 108 char_offset_in_node: 0 109 child_index: ~ 110 utf16_len: 19
··· 8 char_range: 9 - 0 10 - 18 11 + html: "<blockquote>\n<p id=\"p-0-n0\"><span class=\"md-syntax-block\" data-syn-id=\"s0\" data-char-start=\"0\" data-char-end=\"1\">&gt;</span> This is a quote\n</p>\n" 12 offset_map: 13 - byte_range: 14 - 2 ··· 16 char_range: 17 - 0 18 - 0 19 + node_id: p-0-n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 1 29 + node_id: p-0-n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 1 ··· 36 char_range: 37 - 1 38 - 2 39 + node_id: p-0-n0 40 char_offset_in_node: 1 41 child_index: ~ 42 utf16_len: 1 ··· 46 char_range: 47 - 2 48 - 17 49 + node_id: p-0-n0 50 char_offset_in_node: 2 51 child_index: ~ 52 utf16_len: 15 ··· 56 char_range: 57 - 17 58 - 18 59 + node_id: p-0-n0 60 char_offset_in_node: 17 61 child_index: ~ 62 utf16_len: 1 ··· 86 char_range: 87 - 22 88 - 41 89 + html: "<span id=\"p-1-n0\" class=\"md-syntax-block\" data-syn-id=\"s1\" data-char-start=\"18\" data-char-end=\"22\">&gt;\n&gt; </span></span>\n<p id=\"p-1-n1\" dir=\"ltr\">With multiple lines</p>\n" 90 offset_map: 91 - byte_range: 92 + - 18 93 + - 22 94 char_range: 95 + - 18 96 - 22 97 + node_id: p-1-n0 98 + char_offset_in_node: 0 99 + child_index: ~ 100 + utf16_len: 4 101 + - byte_range: 102 - 22 103 + - 22 104 + char_range: 105 + - 22 106 + - 22 107 + node_id: p-1-n1 108 char_offset_in_node: 0 109 child_index: 0 110 utf16_len: 0 111 - byte_range: 112 + - 22 113 + - 41 114 char_range: 115 - 22 116 - 41 117 + node_id: p-1-n1 118 char_offset_in_node: 0 119 child_index: ~ 120 utf16_len: 19
+7 -7
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__bold.snap
··· 8 char_range: 9 - 0 10 - 18 11 - html: "<p id=\"n0\">Some <span class=\"md-syntax-inline\" data-syn-id=\"s0\" data-char-start=\"5\" data-char-end=\"7\">**</span><strong>bold</strong><span class=\"md-syntax-inline\" data-syn-id=\"s1\" data-char-start=\"11\" data-char-end=\"13\">**</span> text</p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 - node_id: n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 5 29 - node_id: n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 5 ··· 36 char_range: 37 - 5 38 - 7 39 - node_id: n0 40 char_offset_in_node: 5 41 child_index: ~ 42 utf16_len: 2 ··· 46 char_range: 47 - 7 48 - 11 49 - node_id: n0 50 char_offset_in_node: 7 51 child_index: ~ 52 utf16_len: 4 ··· 56 char_range: 57 - 11 58 - 13 59 - node_id: n0 60 char_offset_in_node: 11 61 child_index: ~ 62 utf16_len: 2 ··· 66 char_range: 67 - 13 68 - 18 69 - node_id: n0 70 char_offset_in_node: 13 71 child_index: ~ 72 utf16_len: 5
··· 8 char_range: 9 - 0 10 - 18 11 + html: "<p id=\"p-0-n0\" dir=\"ltr\">Some <span class=\"md-syntax-inline\" data-syn-id=\"s0\" data-char-start=\"5\" data-char-end=\"7\">**</span><strong>bold</strong><span class=\"md-syntax-inline\" data-syn-id=\"s1\" data-char-start=\"11\" data-char-end=\"13\">**</span> text</p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 + node_id: p-0-n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 5 29 + node_id: p-0-n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 5 ··· 36 char_range: 37 - 5 38 - 7 39 + node_id: p-0-n0 40 char_offset_in_node: 5 41 child_index: ~ 42 utf16_len: 2 ··· 46 char_range: 47 - 7 48 - 11 49 + node_id: p-0-n0 50 char_offset_in_node: 7 51 child_index: ~ 52 utf16_len: 4 ··· 56 char_range: 57 - 11 58 - 13 59 + node_id: p-0-n0 60 char_offset_in_node: 11 61 child_index: ~ 62 utf16_len: 2 ··· 66 char_range: 67 - 13 68 - 18 69 + node_id: p-0-n0 70 char_offset_in_node: 13 71 child_index: ~ 72 utf16_len: 5
+9 -9
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__bold_italic.snap
··· 8 char_range: 9 - 0 10 - 27 11 - html: "<p id=\"n0\">Some <span class=\"md-syntax-inline\" data-syn-id=\"s0\" data-char-start=\"5\" data-char-end=\"6\">*</span><em><span class=\"md-syntax-inline\" data-syn-id=\"s1\" data-char-start=\"6\" data-char-end=\"8\">**</span><strong>bold italic</strong><span class=\"md-syntax-inline\" data-syn-id=\"s2\" data-char-start=\"19\" data-char-end=\"21\">**</span></em><span class=\"md-syntax-inline\" data-syn-id=\"s3\" data-char-start=\"21\" data-char-end=\"22\">*</span> text</p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 - node_id: n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 5 29 - node_id: n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 5 ··· 36 char_range: 37 - 5 38 - 6 39 - node_id: n0 40 char_offset_in_node: 5 41 child_index: ~ 42 utf16_len: 1 ··· 46 char_range: 47 - 6 48 - 8 49 - node_id: n0 50 char_offset_in_node: 6 51 child_index: ~ 52 utf16_len: 2 ··· 56 char_range: 57 - 8 58 - 19 59 - node_id: n0 60 char_offset_in_node: 8 61 child_index: ~ 62 utf16_len: 11 ··· 66 char_range: 67 - 19 68 - 21 69 - node_id: n0 70 char_offset_in_node: 19 71 child_index: ~ 72 utf16_len: 2 ··· 76 char_range: 77 - 21 78 - 22 79 - node_id: n0 80 char_offset_in_node: 21 81 child_index: ~ 82 utf16_len: 1 ··· 86 char_range: 87 - 22 88 - 27 89 - node_id: n0 90 char_offset_in_node: 22 91 child_index: ~ 92 utf16_len: 5
··· 8 char_range: 9 - 0 10 - 27 11 + html: "<p id=\"p-0-n0\" dir=\"ltr\">Some <span class=\"md-syntax-inline\" data-syn-id=\"s0\" data-char-start=\"5\" data-char-end=\"6\">*</span><em><span class=\"md-syntax-inline\" data-syn-id=\"s1\" data-char-start=\"6\" data-char-end=\"8\">**</span><strong>bold italic</strong><span class=\"md-syntax-inline\" data-syn-id=\"s2\" data-char-start=\"19\" data-char-end=\"21\">**</span></em><span class=\"md-syntax-inline\" data-syn-id=\"s3\" data-char-start=\"21\" data-char-end=\"22\">*</span> text</p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 + node_id: p-0-n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 5 29 + node_id: p-0-n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 5 ··· 36 char_range: 37 - 5 38 - 6 39 + node_id: p-0-n0 40 char_offset_in_node: 5 41 child_index: ~ 42 utf16_len: 1 ··· 46 char_range: 47 - 6 48 - 8 49 + node_id: p-0-n0 50 char_offset_in_node: 6 51 child_index: ~ 52 utf16_len: 2 ··· 56 char_range: 57 - 8 58 - 19 59 + node_id: p-0-n0 60 char_offset_in_node: 8 61 child_index: ~ 62 utf16_len: 11 ··· 66 char_range: 67 - 19 68 - 21 69 + node_id: p-0-n0 70 char_offset_in_node: 19 71 child_index: ~ 72 utf16_len: 2 ··· 76 char_range: 77 - 21 78 - 22 79 + node_id: p-0-n0 80 char_offset_in_node: 21 81 child_index: ~ 82 utf16_len: 1 ··· 86 char_range: 87 - 22 88 - 27 89 + node_id: p-0-n0 90 char_offset_in_node: 22 91 child_index: ~ 92 utf16_len: 5
+2 -2
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__code_block_fenced.snap
··· 8 char_range: 9 - 0 10 - 24 11 - html: "<span class=\"md-syntax-block\" data-syn-id=\"s0\" data-char-start=\"0\" data-char-end=\"8\" spellcheck=\"false\">```rust</span>\n<pre data-node-id=\"n0\"><code class=\"wvc-code language-Rust\"><span class=\"wvc-source wvc-rust\"><span class=\"wvc-meta wvc-function wvc-rust\"><span class=\"wvc-meta wvc-function wvc-rust\"><span class=\"wvc-storage wvc-type wvc-function wvc-rust\">fn</span> </span><span class=\"wvc-entity wvc-name wvc-function wvc-rust\">main</span></span><span class=\"wvc-meta wvc-function wvc-rust\"><span class=\"wvc-meta wvc-function wvc-parameters wvc-rust\"><span class=\"wvc-punctuation wvc-section wvc-parameters wvc-begin wvc-rust\">(</span></span><span class=\"wvc-meta wvc-function wvc-rust\"><span class=\"wvc-meta wvc-function wvc-parameters wvc-rust\"><span class=\"wvc-punctuation wvc-section wvc-parameters wvc-end wvc-rust\">)</span></span></span></span><span class=\"wvc-meta wvc-function wvc-rust\"> </span><span class=\"wvc-meta wvc-function wvc-rust\"><span class=\"wvc-meta wvc-block wvc-rust\"><span class=\"wvc-punctuation wvc-section wvc-block wvc-begin wvc-rust\">{</span></span><span class=\"wvc-meta wvc-block wvc-rust\"><span class=\"wvc-punctuation wvc-section wvc-block wvc-end wvc-rust\">}</span></span></span>\n</span></code></pre><span class=\"md-syntax-block\" data-syn-id=\"s1\" data-char-start=\"21\" data-char-end=\"24\" spellcheck=\"false\">```</span>" 12 offset_map: 13 - byte_range: 14 - 8 ··· 16 char_range: 17 - 8 18 - 21 19 - node_id: n0 20 char_offset_in_node: 0 21 child_index: ~ 22 utf16_len: 13
··· 8 char_range: 9 - 0 10 - 24 11 + html: "<span class=\"md-syntax-block\" data-syn-id=\"s0\" data-char-start=\"0\" data-char-end=\"8\" spellcheck=\"false\">```rust</span>\n<pre data-node-id=\"p-0-n0\"><code class=\"wvc-code language-Rust\"><span class=\"wvc-source wvc-rust\"><span class=\"wvc-meta wvc-function wvc-rust\"><span class=\"wvc-meta wvc-function wvc-rust\"><span class=\"wvc-storage wvc-type wvc-function wvc-rust\">fn</span> </span><span class=\"wvc-entity wvc-name wvc-function wvc-rust\">main</span></span><span class=\"wvc-meta wvc-function wvc-rust\"><span class=\"wvc-meta wvc-function wvc-parameters wvc-rust\"><span class=\"wvc-punctuation wvc-section wvc-parameters wvc-begin wvc-rust\">(</span></span><span class=\"wvc-meta wvc-function wvc-rust\"><span class=\"wvc-meta wvc-function wvc-parameters wvc-rust\"><span class=\"wvc-punctuation wvc-section wvc-parameters wvc-end wvc-rust\">)</span></span></span></span><span class=\"wvc-meta wvc-function wvc-rust\"> </span><span class=\"wvc-meta wvc-function wvc-rust\"><span class=\"wvc-meta wvc-block wvc-rust\"><span class=\"wvc-punctuation wvc-section wvc-block wvc-begin wvc-rust\">{</span></span><span class=\"wvc-meta wvc-block wvc-rust\"><span class=\"wvc-punctuation wvc-section wvc-block wvc-end wvc-rust\">}</span></span></span>\n</span></code></pre><span class=\"md-syntax-block\" data-syn-id=\"s1\" data-char-start=\"21\" data-char-end=\"24\" spellcheck=\"false\">```</span>" 12 offset_map: 13 - byte_range: 14 - 8 ··· 16 char_range: 17 - 8 18 - 21 19 + node_id: p-0-n0 20 char_offset_in_node: 0 21 child_index: ~ 22 utf16_len: 13
+22 -12
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__gap_between_blocks.snap
··· 8 char_range: 9 - 0 10 - 10 11 - html: "<h1 data-node-id=\"n0\"><span class=\"md-syntax-block\" data-syn-id=\"s0\" data-char-start=\"0\" data-char-end=\"2\"># </span>Heading\n</h1>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 - node_id: n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 2 29 - node_id: n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 2 ··· 36 char_range: 37 - 2 38 - 9 39 - node_id: n0 40 char_offset_in_node: 2 41 child_index: ~ 42 utf16_len: 7 ··· 46 char_range: 47 - 9 48 - 10 49 - node_id: n0 50 char_offset_in_node: 9 51 child_index: ~ 52 utf16_len: 1 ··· 57 char_range: 58 - 11 59 - 26 60 - html: "<p id=\"n1\">Paragraph below</p>\n" 61 offset_map: 62 - byte_range: 63 - - 0 64 - - 0 65 char_range: 66 - 11 67 - 11 68 - node_id: n1 69 char_offset_in_node: 0 70 child_index: 0 71 utf16_len: 0 72 - byte_range: 73 - - 0 74 - - 15 75 char_range: 76 - 11 77 - 26 78 - node_id: n1 79 char_offset_in_node: 0 80 child_index: ~ 81 utf16_len: 15
··· 8 char_range: 9 - 0 10 - 10 11 + html: "<h1 data-node-id=\"p-0-n0\" dir=\"ltr\"><span class=\"md-syntax-block\" data-syn-id=\"s0\" data-char-start=\"0\" data-char-end=\"2\"># </span>Heading\n</h1>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 + node_id: p-0-n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 2 29 + node_id: p-0-n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 2 ··· 36 char_range: 37 - 2 38 - 9 39 + node_id: p-0-n0 40 char_offset_in_node: 2 41 child_index: ~ 42 utf16_len: 7 ··· 46 char_range: 47 - 9 48 - 10 49 + node_id: p-0-n0 50 char_offset_in_node: 9 51 child_index: ~ 52 utf16_len: 1 ··· 57 char_range: 58 - 11 59 - 26 60 + html: "<span id=\"p-1-n0\">\n</span>\n<p id=\"p-1-n1\" dir=\"ltr\">Paragraph below</p>\n" 61 offset_map: 62 - byte_range: 63 + - 10 64 + - 11 65 + char_range: 66 + - 10 67 + - 11 68 + node_id: p-1-n0 69 + char_offset_in_node: 0 70 + child_index: ~ 71 + utf16_len: 1 72 + - byte_range: 73 + - 11 74 + - 11 75 char_range: 76 - 11 77 - 11 78 + node_id: p-1-n1 79 char_offset_in_node: 0 80 child_index: 0 81 utf16_len: 0 82 - byte_range: 83 + - 11 84 + - 26 85 char_range: 86 - 11 87 - 26 88 + node_id: p-1-n1 89 char_offset_in_node: 0 90 child_index: ~ 91 utf16_len: 15
+6 -6
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__hard_break.snap
··· 8 char_range: 9 - 0 10 - 19 11 - html: "<p id=\"n0\">Line one<span class=\"md-placeholder\" data-syn-id=\"s0\" data-char-start=\"8\" data-char-end=\"10\"> </span><br /> Line two</p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 - node_id: n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 8 29 - node_id: n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 8 ··· 36 char_range: 37 - 8 38 - 10 39 - node_id: n0 40 char_offset_in_node: 8 41 child_index: ~ 42 utf16_len: 2 ··· 46 char_range: 47 - 10 48 - 11 49 - node_id: n0 50 char_offset_in_node: 10 51 child_index: ~ 52 utf16_len: 1 ··· 56 char_range: 57 - 11 58 - 19 59 - node_id: n0 60 char_offset_in_node: 11 61 child_index: ~ 62 utf16_len: 8
··· 8 char_range: 9 - 0 10 - 19 11 + html: "<p id=\"p-0-n0\" dir=\"ltr\">Line one<span class=\"md-placeholder\" data-syn-id=\"s0\" data-char-start=\"8\" data-char-end=\"10\"> </span><br />​Line two</p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 + node_id: p-0-n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 8 29 + node_id: p-0-n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 8 ··· 36 char_range: 37 - 8 38 - 10 39 + node_id: p-0-n0 40 char_offset_in_node: 8 41 child_index: ~ 42 utf16_len: 2 ··· 46 char_range: 47 - 10 48 - 11 49 + node_id: p-0-n0 50 char_offset_in_node: 10 51 child_index: ~ 52 utf16_len: 1 ··· 56 char_range: 57 - 11 58 - 19 59 + node_id: p-0-n0 60 char_offset_in_node: 11 61 child_index: ~ 62 utf16_len: 8
+4 -4
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__heading_h1.snap
··· 8 char_range: 9 - 0 10 - 11 11 - html: "<h1 data-node-id=\"n0\"><span class=\"md-syntax-block\" data-syn-id=\"s0\" data-char-start=\"0\" data-char-end=\"2\"># </span>Heading 1</h1>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 - node_id: n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 2 29 - node_id: n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 2 ··· 36 char_range: 37 - 2 38 - 11 39 - node_id: n0 40 char_offset_in_node: 2 41 child_index: ~ 42 utf16_len: 9
··· 8 char_range: 9 - 0 10 - 11 11 + html: "<h1 data-node-id=\"p-0-n0\" dir=\"ltr\"><span class=\"md-syntax-block\" data-syn-id=\"s0\" data-char-start=\"0\" data-char-end=\"2\"># </span>Heading 1</h1>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 + node_id: p-0-n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 2 29 + node_id: p-0-n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 2 ··· 36 char_range: 37 - 2 38 - 11 39 + node_id: p-0-n0 40 char_offset_in_node: 2 41 child_index: ~ 42 utf16_len: 9
+71 -41
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__heading_levels.snap
··· 8 char_range: 9 - 0 10 - 5 11 - html: "<h1 data-node-id=\"n0\"><span class=\"md-syntax-block\" data-syn-id=\"s0\" data-char-start=\"0\" data-char-end=\"2\"># </span>H1\n</h1>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 - node_id: n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 2 29 - node_id: n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 2 ··· 36 char_range: 37 - 2 38 - 4 39 - node_id: n0 40 char_offset_in_node: 2 41 child_index: ~ 42 utf16_len: 2 ··· 46 char_range: 47 - 4 48 - 5 49 - node_id: n0 50 char_offset_in_node: 4 51 child_index: ~ 52 utf16_len: 1 ··· 57 char_range: 58 - 6 59 - 12 60 - html: "<h2 data-node-id=\"n1\"><span class=\"md-syntax-block\" data-syn-id=\"s1\" data-char-start=\"6\" data-char-end=\"9\">## </span>H2\n</h2>\n" 61 offset_map: 62 - byte_range: 63 - - 0 64 - - 0 65 char_range: 66 - 6 67 - 6 68 - node_id: n1 69 char_offset_in_node: 0 70 child_index: 0 71 utf16_len: 0 72 - byte_range: 73 - - 0 74 - - 3 75 char_range: 76 - 6 77 - 9 78 - node_id: n1 79 char_offset_in_node: 0 80 child_index: ~ 81 utf16_len: 3 82 - byte_range: 83 - - 3 84 - - 5 85 char_range: 86 - 9 87 - 11 88 - node_id: n1 89 char_offset_in_node: 3 90 child_index: ~ 91 utf16_len: 2 92 - byte_range: 93 - - 5 94 - - 6 95 char_range: 96 - 11 97 - 12 98 - node_id: n1 99 char_offset_in_node: 5 100 child_index: ~ 101 utf16_len: 1 ··· 106 char_range: 107 - 13 108 - 20 109 - html: "<h3 data-node-id=\"n2\"><span class=\"md-syntax-block\" data-syn-id=\"s2\" data-char-start=\"13\" data-char-end=\"17\">### </span>H3\n</h3>\n" 110 offset_map: 111 - byte_range: 112 - - 0 113 - - 0 114 char_range: 115 - 13 116 - 13 117 - node_id: n2 118 char_offset_in_node: 0 119 child_index: 0 120 utf16_len: 0 121 - byte_range: 122 - - 0 123 - - 4 124 char_range: 125 - 13 126 - 17 127 - node_id: n2 128 char_offset_in_node: 0 129 child_index: ~ 130 utf16_len: 4 131 - byte_range: 132 - - 4 133 - - 6 134 char_range: 135 - 17 136 - 19 137 - node_id: n2 138 char_offset_in_node: 4 139 child_index: ~ 140 utf16_len: 2 141 - byte_range: 142 - - 6 143 - - 7 144 char_range: 145 - 19 146 - 20 147 - node_id: n2 148 char_offset_in_node: 6 149 child_index: ~ 150 utf16_len: 1 ··· 155 char_range: 156 - 21 157 - 28 158 - html: "<h4 data-node-id=\"n3\"><span class=\"md-syntax-block\" data-syn-id=\"s3\" data-char-start=\"21\" data-char-end=\"26\">#### </span>H4</h4>\n" 159 offset_map: 160 - byte_range: 161 - - 0 162 - - 0 163 char_range: 164 - 21 165 - 21 166 - node_id: n3 167 char_offset_in_node: 0 168 child_index: 0 169 utf16_len: 0 170 - byte_range: 171 - - 0 172 - - 5 173 char_range: 174 - 21 175 - 26 176 - node_id: n3 177 char_offset_in_node: 0 178 child_index: ~ 179 utf16_len: 5 180 - byte_range: 181 - - 5 182 - - 7 183 char_range: 184 - 26 185 - 28 186 - node_id: n3 187 char_offset_in_node: 5 188 child_index: ~ 189 utf16_len: 2
··· 8 char_range: 9 - 0 10 - 5 11 + html: "<h1 data-node-id=\"p-0-n0\" dir=\"ltr\"><span class=\"md-syntax-block\" data-syn-id=\"s0\" data-char-start=\"0\" data-char-end=\"2\"># </span>H1\n</h1>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 + node_id: p-0-n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 2 29 + node_id: p-0-n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 2 ··· 36 char_range: 37 - 2 38 - 4 39 + node_id: p-0-n0 40 char_offset_in_node: 2 41 child_index: ~ 42 utf16_len: 2 ··· 46 char_range: 47 - 4 48 - 5 49 + node_id: p-0-n0 50 char_offset_in_node: 4 51 child_index: ~ 52 utf16_len: 1 ··· 57 char_range: 58 - 6 59 - 12 60 + html: "<span id=\"p-1-n0\">\n</span>\n<h2 data-node-id=\"p-1-n1\" dir=\"ltr\"><span class=\"md-syntax-block\" data-syn-id=\"s1\" data-char-start=\"6\" data-char-end=\"9\">## </span>H2\n</h2>\n" 61 offset_map: 62 - byte_range: 63 + - 5 64 + - 6 65 char_range: 66 + - 5 67 - 6 68 + node_id: p-1-n0 69 + char_offset_in_node: 0 70 + child_index: ~ 71 + utf16_len: 1 72 + - byte_range: 73 + - 6 74 + - 6 75 + char_range: 76 - 6 77 + - 6 78 + node_id: p-1-n1 79 char_offset_in_node: 0 80 child_index: 0 81 utf16_len: 0 82 - byte_range: 83 + - 6 84 + - 9 85 char_range: 86 - 6 87 - 9 88 + node_id: p-1-n1 89 char_offset_in_node: 0 90 child_index: ~ 91 utf16_len: 3 92 - byte_range: 93 + - 9 94 + - 11 95 char_range: 96 - 9 97 - 11 98 + node_id: p-1-n1 99 char_offset_in_node: 3 100 child_index: ~ 101 utf16_len: 2 102 - byte_range: 103 + - 11 104 + - 12 105 char_range: 106 - 11 107 - 12 108 + node_id: p-1-n1 109 char_offset_in_node: 5 110 child_index: ~ 111 utf16_len: 1 ··· 116 char_range: 117 - 13 118 - 20 119 + html: "<span id=\"p-2-n0\">\n</span>\n<h3 data-node-id=\"p-2-n1\" dir=\"ltr\"><span class=\"md-syntax-block\" data-syn-id=\"s2\" data-char-start=\"13\" data-char-end=\"17\">### </span>H3\n</h3>\n" 120 offset_map: 121 - byte_range: 122 + - 12 123 + - 13 124 char_range: 125 + - 12 126 - 13 127 + node_id: p-2-n0 128 + char_offset_in_node: 0 129 + child_index: ~ 130 + utf16_len: 1 131 + - byte_range: 132 - 13 133 + - 13 134 + char_range: 135 + - 13 136 + - 13 137 + node_id: p-2-n1 138 char_offset_in_node: 0 139 child_index: 0 140 utf16_len: 0 141 - byte_range: 142 + - 13 143 + - 17 144 char_range: 145 - 13 146 - 17 147 + node_id: p-2-n1 148 char_offset_in_node: 0 149 child_index: ~ 150 utf16_len: 4 151 - byte_range: 152 + - 17 153 + - 19 154 char_range: 155 - 17 156 - 19 157 + node_id: p-2-n1 158 char_offset_in_node: 4 159 child_index: ~ 160 utf16_len: 2 161 - byte_range: 162 + - 19 163 + - 20 164 char_range: 165 - 19 166 - 20 167 + node_id: p-2-n1 168 char_offset_in_node: 6 169 child_index: ~ 170 utf16_len: 1 ··· 175 char_range: 176 - 21 177 - 28 178 + html: "<span id=\"p-3-n0\">\n</span>\n<h4 data-node-id=\"p-3-n1\" dir=\"ltr\"><span class=\"md-syntax-block\" data-syn-id=\"s3\" data-char-start=\"21\" data-char-end=\"26\">#### </span>H4</h4>\n" 179 offset_map: 180 - byte_range: 181 + - 20 182 + - 21 183 char_range: 184 + - 20 185 + - 21 186 + node_id: p-3-n0 187 + char_offset_in_node: 0 188 + child_index: ~ 189 + utf16_len: 1 190 + - byte_range: 191 - 21 192 - 21 193 + char_range: 194 + - 21 195 + - 21 196 + node_id: p-3-n1 197 char_offset_in_node: 0 198 child_index: 0 199 utf16_len: 0 200 - byte_range: 201 + - 21 202 + - 26 203 char_range: 204 - 21 205 - 26 206 + node_id: p-3-n1 207 char_offset_in_node: 0 208 child_index: ~ 209 utf16_len: 5 210 - byte_range: 211 + - 26 212 + - 28 213 char_range: 214 - 26 215 - 28 216 + node_id: p-3-n1 217 char_offset_in_node: 5 218 child_index: ~ 219 utf16_len: 2
+5 -5
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__inline_code.snap
··· 8 char_range: 9 - 0 10 - 16 11 - html: "<p id=\"n0\">Some <span class=\"md-syntax-inline\" data-syn-id=\"s0\" data-char-start=\"5\" data-char-end=\"6\" spellcheck=\"false\">`</span><code>code</code><span class=\"md-syntax-inline\" data-syn-id=\"s1\" data-char-start=\"10\" data-char-end=\"11\" spellcheck=\"false\">`</span> here</p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 - node_id: n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 5 29 - node_id: n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 5 ··· 36 char_range: 37 - 6 38 - 10 39 - node_id: n0 40 char_offset_in_node: 5 41 child_index: ~ 42 utf16_len: 6 ··· 46 char_range: 47 - 11 48 - 16 49 - node_id: n0 50 char_offset_in_node: 11 51 child_index: ~ 52 utf16_len: 5
··· 8 char_range: 9 - 0 10 - 16 11 + html: "<p id=\"p-0-n0\" dir=\"ltr\">Some <span class=\"md-syntax-inline\" data-syn-id=\"s0\" data-char-start=\"5\" data-char-end=\"6\" spellcheck=\"false\">`</span><code>code</code><span class=\"md-syntax-inline\" data-syn-id=\"s1\" data-char-start=\"10\" data-char-end=\"11\" spellcheck=\"false\">`</span> here</p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 + node_id: p-0-n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 5 29 + node_id: p-0-n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 5 ··· 36 char_range: 37 - 6 38 - 10 39 + node_id: p-0-n0 40 char_offset_in_node: 5 41 child_index: ~ 42 utf16_len: 6 ··· 46 char_range: 47 - 11 48 - 16 49 + node_id: p-0-n0 50 char_offset_in_node: 11 51 child_index: ~ 52 utf16_len: 5
+7 -7
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__italic.snap
··· 8 char_range: 9 - 0 10 - 18 11 - html: "<p id=\"n0\">Some <span class=\"md-syntax-inline\" data-syn-id=\"s0\" data-char-start=\"5\" data-char-end=\"6\">*</span><em>italic</em><span class=\"md-syntax-inline\" data-syn-id=\"s1\" data-char-start=\"12\" data-char-end=\"13\">*</span> text</p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 - node_id: n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 5 29 - node_id: n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 5 ··· 36 char_range: 37 - 5 38 - 6 39 - node_id: n0 40 char_offset_in_node: 5 41 child_index: ~ 42 utf16_len: 1 ··· 46 char_range: 47 - 6 48 - 12 49 - node_id: n0 50 char_offset_in_node: 6 51 child_index: ~ 52 utf16_len: 6 ··· 56 char_range: 57 - 12 58 - 13 59 - node_id: n0 60 char_offset_in_node: 12 61 child_index: ~ 62 utf16_len: 1 ··· 66 char_range: 67 - 13 68 - 18 69 - node_id: n0 70 char_offset_in_node: 13 71 child_index: ~ 72 utf16_len: 5
··· 8 char_range: 9 - 0 10 - 18 11 + html: "<p id=\"p-0-n0\" dir=\"ltr\">Some <span class=\"md-syntax-inline\" data-syn-id=\"s0\" data-char-start=\"5\" data-char-end=\"6\">*</span><em>italic</em><span class=\"md-syntax-inline\" data-syn-id=\"s1\" data-char-start=\"12\" data-char-end=\"13\">*</span> text</p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 + node_id: p-0-n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 5 29 + node_id: p-0-n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 5 ··· 36 char_range: 37 - 5 38 - 6 39 + node_id: p-0-n0 40 char_offset_in_node: 5 41 child_index: ~ 42 utf16_len: 1 ··· 46 char_range: 47 - 6 48 - 12 49 + node_id: p-0-n0 50 char_offset_in_node: 6 51 child_index: ~ 52 utf16_len: 6 ··· 56 char_range: 57 - 12 58 - 13 59 + node_id: p-0-n0 60 char_offset_in_node: 12 61 child_index: ~ 62 utf16_len: 1 ··· 66 char_range: 67 - 13 68 - 18 69 + node_id: p-0-n0 70 char_offset_in_node: 13 71 child_index: ~ 72 utf16_len: 5
+3 -3
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__mixed_unicode_ascii.snap
··· 8 char_range: 9 - 0 10 - 16 11 - html: "<p id=\"n0\">Hello 你好 world 🎉</p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 - node_id: n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 16 29 - node_id: n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 17
··· 8 char_range: 9 - 0 10 - 16 11 + html: "<p id=\"p-0-n0\" dir=\"ltr\">Hello 你好 world 🎉</p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 + node_id: p-0-n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 16 29 + node_id: p-0-n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 17
+21 -11
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__multiple_blank_lines.snap
··· 8 char_range: 9 - 0 10 - 6 11 - html: "<p id=\"n0\">First\n</p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 - node_id: n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 5 29 - node_id: n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 5 ··· 36 char_range: 37 - 5 38 - 6 39 - node_id: n0 40 char_offset_in_node: 5 41 child_index: ~ 42 utf16_len: 1 ··· 66 char_range: 67 - 9 68 - 15 69 - html: "<p id=\"n1\">Second</p>\n" 70 offset_map: 71 - byte_range: 72 - - 0 73 - - 0 74 char_range: 75 - 9 76 - 9 77 - node_id: n1 78 char_offset_in_node: 0 79 child_index: 0 80 utf16_len: 0 81 - byte_range: 82 - - 0 83 - - 6 84 char_range: 85 - 9 86 - 15 87 - node_id: n1 88 char_offset_in_node: 0 89 child_index: ~ 90 utf16_len: 6
··· 8 char_range: 9 - 0 10 - 6 11 + html: "<p id=\"p-0-n0\" dir=\"ltr\">First\n</p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 + node_id: p-0-n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 5 29 + node_id: p-0-n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 5 ··· 36 char_range: 37 - 5 38 - 6 39 + node_id: p-0-n0 40 char_offset_in_node: 5 41 child_index: ~ 42 utf16_len: 1 ··· 66 char_range: 67 - 9 68 - 15 69 + html: "<span id=\"p-1-n0\">\n\n\n</span>\n<p id=\"p-1-n1\" dir=\"ltr\">Second</p>\n" 70 offset_map: 71 - byte_range: 72 + - 6 73 + - 9 74 char_range: 75 + - 6 76 - 9 77 + node_id: p-1-n0 78 + char_offset_in_node: 0 79 + child_index: ~ 80 + utf16_len: 3 81 + - byte_range: 82 - 9 83 + - 9 84 + char_range: 85 + - 9 86 + - 9 87 + node_id: p-1-n1 88 char_offset_in_node: 0 89 child_index: 0 90 utf16_len: 0 91 - byte_range: 92 + - 9 93 + - 15 94 char_range: 95 - 9 96 - 15 97 + node_id: p-1-n1 98 char_offset_in_node: 0 99 child_index: ~ 100 utf16_len: 6
+11 -11
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__multiple_inline_formats.snap
··· 8 char_range: 9 - 0 10 - 32 11 - html: "<p id=\"n0\"><span class=\"md-syntax-inline\" data-syn-id=\"s0\" data-char-start=\"0\" data-char-end=\"2\">**</span><strong>Bold</strong><span class=\"md-syntax-inline\" data-syn-id=\"s1\" data-char-start=\"6\" data-char-end=\"8\">**</span> and <span class=\"md-syntax-inline\" data-syn-id=\"s2\" data-char-start=\"13\" data-char-end=\"14\">*</span><em>italic</em><span class=\"md-syntax-inline\" data-syn-id=\"s3\" data-char-start=\"20\" data-char-end=\"21\">*</span> and <span class=\"md-syntax-inline\" data-syn-id=\"s4\" data-char-start=\"26\" data-char-end=\"27\" spellcheck=\"false\">`</span><code>code</code><span class=\"md-syntax-inline\" data-syn-id=\"s5\" data-char-start=\"31\" data-char-end=\"32\" spellcheck=\"false\">`</span></p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 - node_id: n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 2 29 - node_id: n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 2 ··· 36 char_range: 37 - 2 38 - 6 39 - node_id: n0 40 char_offset_in_node: 2 41 child_index: ~ 42 utf16_len: 4 ··· 46 char_range: 47 - 6 48 - 8 49 - node_id: n0 50 char_offset_in_node: 6 51 child_index: ~ 52 utf16_len: 2 ··· 56 char_range: 57 - 8 58 - 13 59 - node_id: n0 60 char_offset_in_node: 8 61 child_index: ~ 62 utf16_len: 5 ··· 66 char_range: 67 - 13 68 - 14 69 - node_id: n0 70 char_offset_in_node: 13 71 child_index: ~ 72 utf16_len: 1 ··· 76 char_range: 77 - 14 78 - 20 79 - node_id: n0 80 char_offset_in_node: 14 81 child_index: ~ 82 utf16_len: 6 ··· 86 char_range: 87 - 20 88 - 21 89 - node_id: n0 90 char_offset_in_node: 20 91 child_index: ~ 92 utf16_len: 1 ··· 96 char_range: 97 - 21 98 - 26 99 - node_id: n0 100 char_offset_in_node: 21 101 child_index: ~ 102 utf16_len: 5 ··· 106 char_range: 107 - 27 108 - 31 109 - node_id: n0 110 char_offset_in_node: 26 111 child_index: ~ 112 utf16_len: 6
··· 8 char_range: 9 - 0 10 - 32 11 + html: "<p id=\"p-0-n0\" dir=\"ltr\"><span class=\"md-syntax-inline\" data-syn-id=\"s0\" data-char-start=\"0\" data-char-end=\"2\">**</span><strong>Bold</strong><span class=\"md-syntax-inline\" data-syn-id=\"s1\" data-char-start=\"6\" data-char-end=\"8\">**</span> and <span class=\"md-syntax-inline\" data-syn-id=\"s2\" data-char-start=\"13\" data-char-end=\"14\">*</span><em>italic</em><span class=\"md-syntax-inline\" data-syn-id=\"s3\" data-char-start=\"20\" data-char-end=\"21\">*</span> and <span class=\"md-syntax-inline\" data-syn-id=\"s4\" data-char-start=\"26\" data-char-end=\"27\" spellcheck=\"false\">`</span><code>code</code><span class=\"md-syntax-inline\" data-syn-id=\"s5\" data-char-start=\"31\" data-char-end=\"32\" spellcheck=\"false\">`</span></p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 + node_id: p-0-n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 2 29 + node_id: p-0-n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 2 ··· 36 char_range: 37 - 2 38 - 6 39 + node_id: p-0-n0 40 char_offset_in_node: 2 41 child_index: ~ 42 utf16_len: 4 ··· 46 char_range: 47 - 6 48 - 8 49 + node_id: p-0-n0 50 char_offset_in_node: 6 51 child_index: ~ 52 utf16_len: 2 ··· 56 char_range: 57 - 8 58 - 13 59 + node_id: p-0-n0 60 char_offset_in_node: 8 61 child_index: ~ 62 utf16_len: 5 ··· 66 char_range: 67 - 13 68 - 14 69 + node_id: p-0-n0 70 char_offset_in_node: 13 71 child_index: ~ 72 utf16_len: 1 ··· 76 char_range: 77 - 14 78 - 20 79 + node_id: p-0-n0 80 char_offset_in_node: 14 81 child_index: ~ 82 utf16_len: 6 ··· 86 char_range: 87 - 20 88 - 21 89 + node_id: p-0-n0 90 char_offset_in_node: 20 91 child_index: ~ 92 utf16_len: 1 ··· 96 char_range: 97 - 21 98 - 26 99 + node_id: p-0-n0 100 char_offset_in_node: 21 101 child_index: ~ 102 utf16_len: 5 ··· 106 char_range: 107 - 27 108 - 31 109 + node_id: p-0-n0 110 char_offset_in_node: 26 111 child_index: ~ 112 utf16_len: 6
+59 -19
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__nested_list.snap
··· 27 char_range: 28 - 11 29 - 33 30 - html: "<ul>\n<li data-node-id=\"n0\"><span class=\"md-syntax-block\" data-syn-id=\"s0\" data-char-start=\"11\" data-char-end=\"13\" spellcheck=\"false\">- </span>Child 1\n \n<ul>\n<li data-node-id=\"n1\"><span class=\"md-syntax-block\" data-syn-id=\"s1\" data-char-start=\"23\" data-char-end=\"25\" spellcheck=\"false\">- </span>Child 2\n</li>\n</ul>\n</li>\n</ul>\n" 31 offset_map: 32 - byte_range: 33 - 0 34 - 2 35 char_range: 36 - 11 37 - 13 38 - node_id: n0 39 char_offset_in_node: 0 40 child_index: ~ 41 utf16_len: 2 42 - byte_range: 43 - - 2 44 - - 9 45 char_range: 46 - 13 47 - 20 48 - node_id: n0 49 char_offset_in_node: 2 50 child_index: ~ 51 utf16_len: 7 52 - byte_range: 53 - - 9 54 - - 12 55 char_range: 56 - 20 57 - - 23 58 - node_id: n0 59 char_offset_in_node: 9 60 child_index: ~ 61 - utf16_len: 3 62 - byte_range: 63 - - 12 64 - - 14 65 char_range: 66 - 23 67 - 25 68 - node_id: n1 69 char_offset_in_node: 0 70 child_index: ~ 71 utf16_len: 2 72 - byte_range: 73 - - 14 74 - - 21 75 char_range: 76 - 25 77 - 32 78 - node_id: n1 79 char_offset_in_node: 2 80 child_index: ~ 81 utf16_len: 7 82 - byte_range: 83 - - 21 84 - - 22 85 char_range: 86 - 32 87 - 33 88 - node_id: n1 89 char_offset_in_node: 9 90 child_index: ~ 91 utf16_len: 1
··· 27 char_range: 28 - 11 29 - 33 30 + html: "<ul>\n<li data-node-id=\"p-0-n0\"><span class=\"md-syntax-block\" data-syn-id=\"s0\" data-char-start=\"0\" data-char-end=\"2\" spellcheck=\"false\">- </span>Parent\n \n<ul>\n<li data-node-id=\"p-0-n1\"><span class=\"md-syntax-block\" data-syn-id=\"s1\" data-char-start=\"11\" data-char-end=\"13\" spellcheck=\"false\">- </span>Child 1\n</li>\n<span id=\"p-0-n2\"> </span>\n<li data-node-id=\"p-0-n3\"><span class=\"md-syntax-block\" data-syn-id=\"s2\" data-char-start=\"23\" data-char-end=\"25\" spellcheck=\"false\">- </span>Child 2\n</li>\n</ul>\n" 31 offset_map: 32 - byte_range: 33 - 0 34 - 2 35 char_range: 36 + - 0 37 + - 2 38 + node_id: p-0-n0 39 + char_offset_in_node: 0 40 + child_index: ~ 41 + utf16_len: 2 42 + - byte_range: 43 + - 2 44 + - 8 45 + char_range: 46 + - 2 47 + - 8 48 + node_id: p-0-n0 49 + char_offset_in_node: 2 50 + child_index: ~ 51 + utf16_len: 6 52 + - byte_range: 53 + - 8 54 + - 11 55 + char_range: 56 + - 8 57 + - 11 58 + node_id: p-0-n0 59 + char_offset_in_node: 8 60 + child_index: ~ 61 + utf16_len: 3 62 + - byte_range: 63 - 11 64 - 13 65 + char_range: 66 + - 11 67 + - 13 68 + node_id: p-0-n1 69 char_offset_in_node: 0 70 child_index: ~ 71 utf16_len: 2 72 - byte_range: 73 + - 13 74 + - 20 75 char_range: 76 - 13 77 - 20 78 + node_id: p-0-n1 79 char_offset_in_node: 2 80 child_index: ~ 81 utf16_len: 7 82 - byte_range: 83 + - 20 84 + - 21 85 char_range: 86 - 20 87 + - 21 88 + node_id: p-0-n1 89 char_offset_in_node: 9 90 child_index: ~ 91 + utf16_len: 1 92 + - byte_range: 93 + - 21 94 + - 23 95 + char_range: 96 + - 21 97 + - 23 98 + node_id: p-0-n2 99 + char_offset_in_node: 0 100 + child_index: ~ 101 + utf16_len: 2 102 - byte_range: 103 + - 23 104 + - 25 105 char_range: 106 - 23 107 - 25 108 + node_id: p-0-n3 109 char_offset_in_node: 0 110 child_index: ~ 111 utf16_len: 2 112 - byte_range: 113 + - 25 114 + - 32 115 char_range: 116 - 25 117 - 32 118 + node_id: p-0-n3 119 char_offset_in_node: 2 120 child_index: ~ 121 utf16_len: 7 122 - byte_range: 123 + - 32 124 + - 33 125 char_range: 126 - 32 127 - 33 128 + node_id: p-0-n3 129 char_offset_in_node: 9 130 child_index: ~ 131 utf16_len: 1
+9 -9
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__ordered_list.snap
··· 8 char_range: 9 - 0 10 - 27 11 - html: "<ol>\n<li data-node-id=\"n0\"><span class=\"md-syntax-block\" data-syn-id=\"s0\" data-char-start=\"0\" data-char-end=\"3\" spellcheck=\"false\">1. </span>First\n</li>\n<li data-node-id=\"n1\"><span class=\"md-syntax-block\" data-syn-id=\"s1\" data-char-start=\"9\" data-char-end=\"12\" spellcheck=\"false\">2. </span>Second\n</li>\n<li data-node-id=\"n2\"><span class=\"md-syntax-block\" data-syn-id=\"s2\" data-char-start=\"19\" data-char-end=\"22\" spellcheck=\"false\">3. </span>Third</li>\n</ol>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 3 19 - node_id: n0 20 char_offset_in_node: 0 21 child_index: ~ 22 utf16_len: 3 ··· 26 char_range: 27 - 3 28 - 8 29 - node_id: n0 30 char_offset_in_node: 3 31 child_index: ~ 32 utf16_len: 5 ··· 36 char_range: 37 - 8 38 - 9 39 - node_id: n0 40 char_offset_in_node: 8 41 child_index: ~ 42 utf16_len: 1 ··· 46 char_range: 47 - 9 48 - 12 49 - node_id: n1 50 char_offset_in_node: 0 51 child_index: ~ 52 utf16_len: 3 ··· 56 char_range: 57 - 12 58 - 18 59 - node_id: n1 60 char_offset_in_node: 3 61 child_index: ~ 62 utf16_len: 6 ··· 66 char_range: 67 - 18 68 - 19 69 - node_id: n1 70 char_offset_in_node: 9 71 child_index: ~ 72 utf16_len: 1 ··· 76 char_range: 77 - 19 78 - 22 79 - node_id: n2 80 char_offset_in_node: 0 81 child_index: ~ 82 utf16_len: 3 ··· 86 char_range: 87 - 22 88 - 27 89 - node_id: n2 90 char_offset_in_node: 3 91 child_index: ~ 92 utf16_len: 5
··· 8 char_range: 9 - 0 10 - 27 11 + html: "<ol>\n<li data-node-id=\"p-0-n0\"><span class=\"md-syntax-block\" data-syn-id=\"s0\" data-char-start=\"0\" data-char-end=\"3\" spellcheck=\"false\">1. </span>First\n</li>\n<li data-node-id=\"p-0-n1\"><span class=\"md-syntax-block\" data-syn-id=\"s1\" data-char-start=\"9\" data-char-end=\"12\" spellcheck=\"false\">2. </span>Second\n</li>\n<li data-node-id=\"p-0-n2\"><span class=\"md-syntax-block\" data-syn-id=\"s2\" data-char-start=\"19\" data-char-end=\"22\" spellcheck=\"false\">3. </span>Third</li>\n</ol>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 3 19 + node_id: p-0-n0 20 char_offset_in_node: 0 21 child_index: ~ 22 utf16_len: 3 ··· 26 char_range: 27 - 3 28 - 8 29 + node_id: p-0-n0 30 char_offset_in_node: 3 31 child_index: ~ 32 utf16_len: 5 ··· 36 char_range: 37 - 8 38 - 9 39 + node_id: p-0-n0 40 char_offset_in_node: 8 41 child_index: ~ 42 utf16_len: 1 ··· 46 char_range: 47 - 9 48 - 12 49 + node_id: p-0-n1 50 char_offset_in_node: 0 51 child_index: ~ 52 utf16_len: 3 ··· 56 char_range: 57 - 12 58 - 18 59 + node_id: p-0-n1 60 char_offset_in_node: 3 61 child_index: ~ 62 utf16_len: 6 ··· 66 char_range: 67 - 18 68 - 19 69 + node_id: p-0-n1 70 char_offset_in_node: 9 71 child_index: ~ 72 utf16_len: 1 ··· 76 char_range: 77 - 19 78 - 22 79 + node_id: p-0-n2 80 char_offset_in_node: 0 81 child_index: ~ 82 utf16_len: 3 ··· 86 char_range: 87 - 22 88 - 27 89 + node_id: p-0-n2 90 char_offset_in_node: 3 91 child_index: ~ 92 utf16_len: 5
+3 -3
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__single_paragraph.snap
··· 8 char_range: 9 - 0 10 - 11 11 - html: "<p id=\"n0\">Hello world</p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 - node_id: n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 11 29 - node_id: n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 11
··· 8 char_range: 9 - 0 10 - 11 11 + html: "<p id=\"p-0-n0\" dir=\"ltr\">Hello world</p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 + node_id: p-0-n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 11 29 + node_id: p-0-n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 11
+41 -21
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__three_paragraphs.snap
··· 8 char_range: 9 - 0 10 - 5 11 - html: "<p id=\"n0\">One.\n</p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 - node_id: n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 4 29 - node_id: n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 4 ··· 36 char_range: 37 - 4 38 - 5 39 - node_id: n0 40 char_offset_in_node: 4 41 child_index: ~ 42 utf16_len: 1 ··· 47 char_range: 48 - 6 49 - 11 50 - html: "<p id=\"n1\">Two.\n</p>\n" 51 offset_map: 52 - byte_range: 53 - - 0 54 - - 0 55 char_range: 56 - 6 57 - 6 58 - node_id: n1 59 char_offset_in_node: 0 60 child_index: 0 61 utf16_len: 0 62 - byte_range: 63 - - 0 64 - - 4 65 char_range: 66 - 6 67 - 10 68 - node_id: n1 69 char_offset_in_node: 0 70 child_index: ~ 71 utf16_len: 4 72 - byte_range: 73 - - 4 74 - - 5 75 char_range: 76 - 10 77 - 11 78 - node_id: n1 79 char_offset_in_node: 4 80 child_index: ~ 81 utf16_len: 1 ··· 86 char_range: 87 - 12 88 - 18 89 - html: "<p id=\"n2\">Three.</p>\n" 90 offset_map: 91 - byte_range: 92 - - 0 93 - - 0 94 char_range: 95 - 12 96 - 12 97 - node_id: n2 98 char_offset_in_node: 0 99 child_index: 0 100 utf16_len: 0 101 - byte_range: 102 - - 0 103 - - 6 104 char_range: 105 - 12 106 - 18 107 - node_id: n2 108 char_offset_in_node: 0 109 child_index: ~ 110 utf16_len: 6
··· 8 char_range: 9 - 0 10 - 5 11 + html: "<p id=\"p-0-n0\" dir=\"ltr\">One.\n</p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 + node_id: p-0-n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 4 29 + node_id: p-0-n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 4 ··· 36 char_range: 37 - 4 38 - 5 39 + node_id: p-0-n0 40 char_offset_in_node: 4 41 child_index: ~ 42 utf16_len: 1 ··· 47 char_range: 48 - 6 49 - 11 50 + html: "<span id=\"p-1-n0\">\n</span>\n<p id=\"p-1-n1\" dir=\"ltr\">Two.\n</p>\n" 51 offset_map: 52 - byte_range: 53 + - 5 54 + - 6 55 + char_range: 56 + - 5 57 + - 6 58 + node_id: p-1-n0 59 + char_offset_in_node: 0 60 + child_index: ~ 61 + utf16_len: 1 62 + - byte_range: 63 + - 6 64 + - 6 65 char_range: 66 - 6 67 - 6 68 + node_id: p-1-n1 69 char_offset_in_node: 0 70 child_index: 0 71 utf16_len: 0 72 - byte_range: 73 + - 6 74 + - 10 75 char_range: 76 - 6 77 - 10 78 + node_id: p-1-n1 79 char_offset_in_node: 0 80 child_index: ~ 81 utf16_len: 4 82 - byte_range: 83 + - 10 84 + - 11 85 char_range: 86 - 10 87 - 11 88 + node_id: p-1-n1 89 char_offset_in_node: 4 90 child_index: ~ 91 utf16_len: 1 ··· 96 char_range: 97 - 12 98 - 18 99 + html: "<span id=\"p-2-n0\">\n</span>\n<p id=\"p-2-n1\" dir=\"ltr\">Three.</p>\n" 100 offset_map: 101 - byte_range: 102 + - 11 103 + - 12 104 + char_range: 105 + - 11 106 + - 12 107 + node_id: p-2-n0 108 + char_offset_in_node: 0 109 + child_index: ~ 110 + utf16_len: 1 111 + - byte_range: 112 + - 12 113 + - 12 114 char_range: 115 - 12 116 - 12 117 + node_id: p-2-n1 118 char_offset_in_node: 0 119 child_index: 0 120 utf16_len: 0 121 - byte_range: 122 + - 12 123 + - 18 124 char_range: 125 - 12 126 - 18 127 + node_id: p-2-n1 128 char_offset_in_node: 0 129 child_index: ~ 130 utf16_len: 6
+4 -4
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__trailing_double_newline.snap
··· 8 char_range: 9 - 0 10 - 6 11 - html: "<p id=\"n0\">Hello\n</p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 - node_id: n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 5 29 - node_id: n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 5 ··· 36 char_range: 37 - 5 38 - 6 39 - node_id: n0 40 char_offset_in_node: 5 41 child_index: ~ 42 utf16_len: 1
··· 8 char_range: 9 - 0 10 - 6 11 + html: "<p id=\"p-0-n0\" dir=\"ltr\">Hello\n</p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 + node_id: p-0-n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 5 29 + node_id: p-0-n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 5 ··· 36 char_range: 37 - 5 38 - 6 39 + node_id: p-0-n0 40 char_offset_in_node: 5 41 child_index: ~ 42 utf16_len: 1
+4 -4
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__trailing_single_newline.snap
··· 8 char_range: 9 - 0 10 - 6 11 - html: "<p id=\"n0\">Hello\n</p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 - node_id: n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 5 29 - node_id: n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 5 ··· 36 char_range: 37 - 5 38 - 6 39 - node_id: n0 40 char_offset_in_node: 5 41 child_index: ~ 42 utf16_len: 1
··· 8 char_range: 9 - 0 10 - 6 11 + html: "<p id=\"p-0-n0\" dir=\"ltr\">Hello\n</p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 + node_id: p-0-n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 5 29 + node_id: p-0-n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 5 ··· 36 char_range: 37 - 5 38 - 6 39 + node_id: p-0-n0 40 char_offset_in_node: 5 41 child_index: ~ 42 utf16_len: 1
+21 -11
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__two_paragraphs.snap
··· 8 char_range: 9 - 0 10 - 17 11 - html: "<p id=\"n0\">First paragraph.\n</p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 - node_id: n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 16 29 - node_id: n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 16 ··· 36 char_range: 37 - 16 38 - 17 39 - node_id: n0 40 char_offset_in_node: 16 41 child_index: ~ 42 utf16_len: 1 ··· 47 char_range: 48 - 18 49 - 35 50 - html: "<p id=\"n1\">Second paragraph.</p>\n" 51 offset_map: 52 - byte_range: 53 - - 0 54 - - 0 55 char_range: 56 - 18 57 - 18 58 - node_id: n1 59 char_offset_in_node: 0 60 child_index: 0 61 utf16_len: 0 62 - byte_range: 63 - - 0 64 - - 17 65 char_range: 66 - 18 67 - 35 68 - node_id: n1 69 char_offset_in_node: 0 70 child_index: ~ 71 utf16_len: 17
··· 8 char_range: 9 - 0 10 - 17 11 + html: "<p id=\"p-0-n0\" dir=\"ltr\">First paragraph.\n</p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 + node_id: p-0-n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 16 29 + node_id: p-0-n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 16 ··· 36 char_range: 37 - 16 38 - 17 39 + node_id: p-0-n0 40 char_offset_in_node: 16 41 child_index: ~ 42 utf16_len: 1 ··· 47 char_range: 48 - 18 49 - 35 50 + html: "<span id=\"p-1-n0\">\n</span>\n<p id=\"p-1-n1\" dir=\"ltr\">Second paragraph.</p>\n" 51 offset_map: 52 - byte_range: 53 + - 17 54 + - 18 55 char_range: 56 + - 17 57 - 18 58 + node_id: p-1-n0 59 + char_offset_in_node: 0 60 + child_index: ~ 61 + utf16_len: 1 62 + - byte_range: 63 - 18 64 + - 18 65 + char_range: 66 + - 18 67 + - 18 68 + node_id: p-1-n1 69 char_offset_in_node: 0 70 child_index: 0 71 utf16_len: 0 72 - byte_range: 73 + - 18 74 + - 35 75 char_range: 76 - 18 77 - 35 78 + node_id: p-1-n1 79 char_offset_in_node: 0 80 child_index: ~ 81 utf16_len: 17
+3 -3
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__unicode_cjk.snap
··· 8 char_range: 9 - 0 10 - 4 11 - html: "<p id=\"n0\">你好世界</p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 - node_id: n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 4 29 - node_id: n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 4
··· 8 char_range: 9 - 0 10 - 4 11 + html: "<p id=\"p-0-n0\" dir=\"ltr\">你好世界</p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 + node_id: p-0-n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 4 29 + node_id: p-0-n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 4
+3 -3
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__unicode_emoji.snap
··· 8 char_range: 9 - 0 10 - 13 11 - html: "<p id=\"n0\">Hello 🎉 world</p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 - node_id: n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 13 29 - node_id: n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 14
··· 8 char_range: 9 - 0 10 - 13 11 + html: "<p id=\"p-0-n0\" dir=\"ltr\">Hello 🎉 world</p>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 0 19 + node_id: p-0-n0 20 char_offset_in_node: 0 21 child_index: 0 22 utf16_len: 0 ··· 26 char_range: 27 - 0 28 - 13 29 + node_id: p-0-n0 30 char_offset_in_node: 0 31 child_index: ~ 32 utf16_len: 14
+9 -9
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__unordered_list.snap
··· 8 char_range: 9 - 0 10 - 26 11 - html: "<ul>\n<li data-node-id=\"n0\"><span class=\"md-syntax-block\" data-syn-id=\"s0\" data-char-start=\"0\" data-char-end=\"2\" spellcheck=\"false\">- </span>Item 1\n</li>\n<li data-node-id=\"n1\"><span class=\"md-syntax-block\" data-syn-id=\"s1\" data-char-start=\"9\" data-char-end=\"11\" spellcheck=\"false\">- </span>Item 2\n</li>\n<li data-node-id=\"n2\"><span class=\"md-syntax-block\" data-syn-id=\"s2\" data-char-start=\"18\" data-char-end=\"20\" spellcheck=\"false\">- </span>Item 3</li>\n</ul>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 2 19 - node_id: n0 20 char_offset_in_node: 0 21 child_index: ~ 22 utf16_len: 2 ··· 26 char_range: 27 - 2 28 - 8 29 - node_id: n0 30 char_offset_in_node: 2 31 child_index: ~ 32 utf16_len: 6 ··· 36 char_range: 37 - 8 38 - 9 39 - node_id: n0 40 char_offset_in_node: 8 41 child_index: ~ 42 utf16_len: 1 ··· 46 char_range: 47 - 9 48 - 11 49 - node_id: n1 50 char_offset_in_node: 0 51 child_index: ~ 52 utf16_len: 2 ··· 56 char_range: 57 - 11 58 - 17 59 - node_id: n1 60 char_offset_in_node: 2 61 child_index: ~ 62 utf16_len: 6 ··· 66 char_range: 67 - 17 68 - 18 69 - node_id: n1 70 char_offset_in_node: 8 71 child_index: ~ 72 utf16_len: 1 ··· 76 char_range: 77 - 18 78 - 20 79 - node_id: n2 80 char_offset_in_node: 0 81 child_index: ~ 82 utf16_len: 2 ··· 86 char_range: 87 - 20 88 - 26 89 - node_id: n2 90 char_offset_in_node: 2 91 child_index: ~ 92 utf16_len: 6
··· 8 char_range: 9 - 0 10 - 26 11 + html: "<ul>\n<li data-node-id=\"p-0-n0\"><span class=\"md-syntax-block\" data-syn-id=\"s0\" data-char-start=\"0\" data-char-end=\"2\" spellcheck=\"false\">- </span>Item 1\n</li>\n<li data-node-id=\"p-0-n1\"><span class=\"md-syntax-block\" data-syn-id=\"s1\" data-char-start=\"9\" data-char-end=\"11\" spellcheck=\"false\">- </span>Item 2\n</li>\n<li data-node-id=\"p-0-n2\"><span class=\"md-syntax-block\" data-syn-id=\"s2\" data-char-start=\"18\" data-char-end=\"20\" spellcheck=\"false\">- </span>Item 3</li>\n</ul>\n" 12 offset_map: 13 - byte_range: 14 - 0 ··· 16 char_range: 17 - 0 18 - 2 19 + node_id: p-0-n0 20 char_offset_in_node: 0 21 child_index: ~ 22 utf16_len: 2 ··· 26 char_range: 27 - 2 28 - 8 29 + node_id: p-0-n0 30 char_offset_in_node: 2 31 child_index: ~ 32 utf16_len: 6 ··· 36 char_range: 37 - 8 38 - 9 39 + node_id: p-0-n0 40 char_offset_in_node: 8 41 child_index: ~ 42 utf16_len: 1 ··· 46 char_range: 47 - 9 48 - 11 49 + node_id: p-0-n1 50 char_offset_in_node: 0 51 child_index: ~ 52 utf16_len: 2 ··· 56 char_range: 57 - 11 58 - 17 59 + node_id: p-0-n1 60 char_offset_in_node: 2 61 child_index: ~ 62 utf16_len: 6 ··· 66 char_range: 67 - 17 68 - 18 69 + node_id: p-0-n1 70 char_offset_in_node: 8 71 child_index: ~ 72 utf16_len: 1 ··· 76 char_range: 77 - 18 78 - 20 79 + node_id: p-0-n2 80 char_offset_in_node: 0 81 child_index: ~ 82 utf16_len: 2 ··· 86 char_range: 87 - 20 88 - 26 89 + node_id: p-0-n2 90 char_offset_in_node: 2 91 child_index: ~ 92 utf16_len: 6
+61 -4
crates/weaver-app/src/components/editor/tests.rs
··· 726 char_range: 727 - 0 728 - 6 729 - html: "<blockquote>\n<p id=\"n0\"><span class=\"md-syntax-block\" data-syn-id=\"s0\" data-char-start=\"0\" data-char-end=\"1\">&gt;</span>quote</p>\n</blockquote>\n" 730 offset_map: 731 - byte_range: 732 - 1 ··· 734 char_range: 735 - 0 736 - 0 737 - node_id: n0 738 char_offset_in_node: 0 739 child_index: 0 740 utf16_len: 0 ··· 744 char_range: 745 - 0 746 - 1 747 - node_id: n0 748 char_offset_in_node: 0 749 child_index: ~ 750 utf16_len: 1 ··· 754 char_range: 755 - 1 756 - 6 757 - node_id: n0 758 char_offset_in_node: 1 759 child_index: ~ 760 utf16_len: 5 ··· 1089 ); 1090 } 1091 }
··· 726 char_range: 727 - 0 728 - 6 729 + html: "<blockquote>\n<p id=\"p-0-n0\"><span class=\"md-syntax-block\" data-syn-id=\"s0\" data-char-start=\"0\" data-char-end=\"1\">&gt;</span>quote</p>\n" 730 offset_map: 731 - byte_range: 732 - 1 ··· 734 char_range: 735 - 0 736 - 0 737 + node_id: p-0-n0 738 char_offset_in_node: 0 739 child_index: 0 740 utf16_len: 0 ··· 744 char_range: 745 - 0 746 - 1 747 + node_id: p-0-n0 748 char_offset_in_node: 0 749 child_index: ~ 750 utf16_len: 1 ··· 754 char_range: 755 - 1 756 - 6 757 + node_id: p-0-n0 758 char_offset_in_node: 1 759 child_index: ~ 760 utf16_len: 5 ··· 1089 ); 1090 } 1091 } 1092 + 1093 + // ============================================================================= 1094 + // Text Direction Tests 1095 + // ============================================================================= 1096 + 1097 + #[test] 1098 + fn test_paragraph_dir_ltr() { 1099 + let result = render_test("Hello world"); 1100 + // Verify HTML contains dir="ltr" 1101 + assert!(result[0].html.contains("dir=\"ltr\"")); 1102 + } 1103 + 1104 + #[test] 1105 + fn test_paragraph_dir_rtl_hebrew() { 1106 + let result = render_test("שלום עולם"); 1107 + // Verify HTML contains dir="rtl" 1108 + assert!(result[0].html.contains("dir=\"rtl\"")); 1109 + } 1110 + 1111 + #[test] 1112 + fn test_paragraph_dir_rtl_arabic() { 1113 + let result = render_test("مرحبا بالعالم"); 1114 + // Verify HTML contains dir="rtl" 1115 + assert!(result[0].html.contains("dir=\"rtl\"")); 1116 + } 1117 + 1118 + #[test] 1119 + fn test_paragraph_dir_mixed_leading_neutrals() { 1120 + // Leading numbers and punctuation should be skipped, Hebrew should be detected 1121 + let result = render_test("123... שלום"); 1122 + assert!(result[0].html.contains("dir=\"rtl\"")); 1123 + } 1124 + 1125 + #[test] 1126 + fn test_heading_dir_rtl() { 1127 + let result = render_test("# שלום"); 1128 + // Verify heading has dir="rtl" 1129 + assert!(result[0].html.contains("dir=\"rtl\"")); 1130 + } 1131 + 1132 + #[test] 1133 + fn test_heading_dir_ltr() { 1134 + let result = render_test("# Hello"); 1135 + // Verify heading has dir="ltr" 1136 + assert!(result[0].html.contains("dir=\"ltr\"")); 1137 + } 1138 + 1139 + #[test] 1140 + fn test_multiple_paragraphs_different_directions() { 1141 + let result = render_test("Hello world\n\nשלום עולם\n\nBack to English"); 1142 + // First paragraph should be LTR 1143 + assert!(result[0].html.contains("dir=\"ltr\"")); 1144 + // Second paragraph should be RTL 1145 + assert!(result[1].html.contains("dir=\"rtl\"")); 1146 + // Third paragraph should be LTR 1147 + assert!(result[2].html.contains("dir=\"ltr\"")); 1148 + }
+165 -188
crates/weaver-app/src/components/editor/writer.rs
··· 10 pub mod syntax; 11 pub mod tags; 12 13 use crate::components::editor::writer::segmented::SegmentedWriter; 14 pub use embed::{EditorImageResolver, EmbedContentProvider, ImageResolver}; 15 - pub use syntax::{SyntaxSpanInfo, SyntaxType}; 16 - 17 - #[allow(unused_imports)] 18 - use super::offset_map::{OffsetMapping, RenderResult}; 19 use loro::LoroText; 20 use markdown_weaver::{Alignment, CowStr, Event, WeaverAttributes}; 21 use markdown_weaver_escape::{StrWrite, escape_html, escape_html_body_text_with_char_count}; 22 use std::collections::HashMap; 23 use std::fmt; 24 use std::ops::Range; 25 use weaver_common::EntryIndex; 26 27 /// Result of rendering with the EditorWriter. ··· 117 paragraph_ranges: Vec<(Range<usize>, Range<usize>)>, // (byte_range, char_range) 118 current_paragraph_start: Option<(usize, usize)>, // (byte_offset, char_offset) 119 list_depth: usize, // Track nesting depth to avoid paragraph boundary override inside lists 120 121 // Syntax span tracking for conditional visibility - current paragraph 122 syntax_spans: Vec<SyntaxSpanInfo>, ··· 157 { 158 pub fn new(source: &'a str, source_text: &'a LoroText, events: I) -> Self { 159 Self::new_with_all_offsets(source, source_text, events, 0, 0, 0, 0) 160 - } 161 - 162 - pub fn new_with_all_offsets( 163 - source: &'a str, 164 - source_text: &'a LoroText, 165 - events: I, 166 - node_id_offset: usize, 167 - syn_id_offset: usize, 168 - char_offset_base: usize, 169 - byte_offset_base: usize, 170 - ) -> Self { 171 - Self { 172 - source, 173 - source_text, 174 - events, 175 - writer: SegmentedWriter::new(), 176 - last_byte_offset: byte_offset_base, 177 - last_char_offset: char_offset_base, 178 - end_newline: true, 179 - in_non_writing_block: false, 180 - table_state: TableState::Head, 181 - table_alignments: vec![], 182 - table_cell_index: 0, 183 - numbers: HashMap::new(), 184 - embed_provider: None, 185 - image_resolver: None, 186 - entry_index: None, 187 - code_buffer: None, 188 - code_buffer_byte_range: None, 189 - code_buffer_char_range: None, 190 - code_block_char_start: None, 191 - code_block_opening_span_idx: None, 192 - pending_blockquote_range: None, 193 - render_tables_as_markdown: true, 194 - table_start_offset: None, 195 - offset_maps: Vec::new(), 196 - node_id_prefix: None, 197 - auto_increment_prefix: None, 198 - static_prefix_override: None, 199 - current_paragraph_index: 0, 200 - next_node_id: node_id_offset, 201 - current_node_id: None, 202 - current_node_char_offset: 0, 203 - current_node_child_count: 0, 204 - utf16_checkpoints: vec![(0, 0)], 205 - paragraph_ranges: Vec::new(), 206 - current_paragraph_start: None, 207 - list_depth: 0, 208 - syntax_spans: Vec::new(), 209 - next_syn_id: syn_id_offset, 210 - pending_inline_formats: Vec::new(), 211 - ref_collector: weaver_common::RefCollector::new(), 212 - offset_maps_by_para: Vec::new(), 213 - syntax_spans_by_para: Vec::new(), 214 - refs_by_para: Vec::new(), 215 - pending_block_attrs: None, 216 - active_wrapper: None, 217 - weaver_block_buffer: String::new(), 218 - weaver_block_char_start: None, 219 - footnote_ref_spans: HashMap::new(), 220 - current_footnote_def: None, 221 - _phantom: std::marker::PhantomData, 222 - } 223 - } 224 - 225 - /// Add an embed content provider 226 - pub fn with_embed_provider(mut self, provider: E) -> EditorWriter<'a, I, E, R> { 227 - self.embed_provider = Some(provider); 228 - self 229 - } 230 - 231 - /// Add an image resolver for mapping markdown image URLs to CDN URLs 232 - pub fn with_image_resolver<R2: ImageResolver>( 233 - self, 234 - resolver: R2, 235 - ) -> EditorWriter<'a, I, E, R2> { 236 - EditorWriter { 237 - source: self.source, 238 - source_text: self.source_text, 239 - events: self.events, 240 - writer: self.writer, 241 - last_byte_offset: self.last_byte_offset, 242 - last_char_offset: self.last_char_offset, 243 - end_newline: self.end_newline, 244 - in_non_writing_block: self.in_non_writing_block, 245 - table_state: self.table_state, 246 - table_alignments: self.table_alignments, 247 - table_cell_index: self.table_cell_index, 248 - numbers: self.numbers, 249 - embed_provider: self.embed_provider, 250 - image_resolver: Some(resolver), 251 - entry_index: self.entry_index, 252 - code_buffer: self.code_buffer, 253 - code_buffer_byte_range: self.code_buffer_byte_range, 254 - code_buffer_char_range: self.code_buffer_char_range, 255 - code_block_char_start: self.code_block_char_start, 256 - code_block_opening_span_idx: self.code_block_opening_span_idx, 257 - pending_blockquote_range: self.pending_blockquote_range, 258 - render_tables_as_markdown: self.render_tables_as_markdown, 259 - table_start_offset: self.table_start_offset, 260 - offset_maps: self.offset_maps, 261 - node_id_prefix: self.node_id_prefix, 262 - auto_increment_prefix: self.auto_increment_prefix, 263 - static_prefix_override: self.static_prefix_override, 264 - current_paragraph_index: self.current_paragraph_index, 265 - next_node_id: self.next_node_id, 266 - current_node_id: self.current_node_id, 267 - current_node_char_offset: self.current_node_char_offset, 268 - current_node_child_count: self.current_node_child_count, 269 - utf16_checkpoints: self.utf16_checkpoints, 270 - paragraph_ranges: self.paragraph_ranges, 271 - current_paragraph_start: self.current_paragraph_start, 272 - list_depth: self.list_depth, 273 - syntax_spans: self.syntax_spans, 274 - next_syn_id: self.next_syn_id, 275 - pending_inline_formats: self.pending_inline_formats, 276 - ref_collector: self.ref_collector, 277 - offset_maps_by_para: self.offset_maps_by_para, 278 - syntax_spans_by_para: self.syntax_spans_by_para, 279 - refs_by_para: self.refs_by_para, 280 - pending_block_attrs: self.pending_block_attrs, 281 - active_wrapper: self.active_wrapper, 282 - weaver_block_buffer: self.weaver_block_buffer, 283 - weaver_block_char_start: self.weaver_block_char_start, 284 - footnote_ref_spans: self.footnote_ref_spans, 285 - current_footnote_def: self.current_footnote_def, 286 - _phantom: std::marker::PhantomData, 287 - } 288 - } 289 - 290 - /// Add an entry index for wikilink resolution feedback 291 - pub fn with_entry_index(mut self, index: &'a EntryIndex) -> Self { 292 - self.entry_index = Some(index); 293 - self 294 - } 295 - 296 - /// Set a prefix for node IDs (typically the paragraph ID). 297 - /// This makes node IDs paragraph-scoped and stable across re-renders. 298 - /// Use this for single-paragraph renders where the paragraph ID is known. 299 - pub fn with_node_id_prefix(mut self, prefix: &str) -> Self { 300 - self.node_id_prefix = Some(prefix.to_string()); 301 - self.next_node_id = 0; // Reset counter since each paragraph is independent 302 - self 303 - } 304 - 305 - /// Enable auto-incrementing paragraph prefixes for multi-paragraph renders. 306 - /// Each paragraph gets prefix "p-{N}" where N starts at `start_id` and increments. 307 - /// Node IDs reset to 0 for each paragraph, giving "p-{N}-n0", "p-{N}-n1", etc. 308 - pub fn with_auto_incrementing_prefix(mut self, start_id: usize) -> Self { 309 - self.auto_increment_prefix = Some(start_id); 310 - self.node_id_prefix = Some(format!("p-{}", start_id)); 311 - self.next_node_id = 0; 312 - self 313 } 314 315 /// Get the next paragraph ID that would be assigned (for tracking allocations). ··· 1139 1140 // Emit space for cursor positioning - this gives the browser somewhere 1141 // to place the cursor when navigating to this line 1142 - self.write(" ")?; 1143 self.current_node_child_count += 1; 1144 1145 // Map the space to the newline position - cursor landing here means ··· 1193 self.current_node_child_count += 1; 1194 1195 // After <br>, emit plain zero-width space for cursor positioning 1196 - self.write(" ")?; 1197 1198 // Count the zero-width space text node as a child 1199 self.current_node_child_count += 1; ··· 1215 self.current_node_char_offset += 1; 1216 } 1217 1218 - // DO NOT increment last_char_offset - zero-width space is not in source 1219 - // The \n itself IS in source, so we already accounted for it 1220 self.last_char_offset = char_start + spaces_char_len + 1; // +1 for \n 1221 } else { 1222 // Fallback: just <br> ··· 1259 self.write("<div class=\"toggle-block\"><hr /></div>\n")?; 1260 } 1261 FootnoteReference(name) => { 1262 - // Get/create footnote number 1263 - let len = self.numbers.len() + 1; 1264 - let number = *self.numbers.entry(name.to_string()).or_insert(len); 1265 - 1266 - // Emit the [^name] syntax as a hideable syntax span 1267 let raw_text = &self.source[range.clone()]; 1268 let char_start = self.last_char_offset; 1269 let syntax_char_len = raw_text.chars().count(); 1270 let char_end = char_start + syntax_char_len; 1271 - let syn_id = self.gen_syn_id(); 1272 1273 write!( 1274 &mut self.writer, 1275 - "<span class=\"md-syntax-inline\" data-syn-id=\"{}\" data-char-start=\"{}\" data-char-end=\"{}\" spellcheck=\"false\">", 1276 - syn_id, char_start, char_end 1277 )?; 1278 escape_html(&mut self.writer, raw_text)?; 1279 self.write("</span>")?; 1280 1281 - // Track this span for linking with the footnote definition later 1282 - let span_index = self.syntax_spans.len(); 1283 - self.syntax_spans.push(SyntaxSpanInfo { 1284 - syn_id, 1285 - char_range: char_start..char_end, 1286 - syntax_type: SyntaxType::Inline, 1287 - formatted_range: None, // Set when we see the definition 1288 - }); 1289 - self.footnote_ref_spans 1290 - .insert(name.to_string(), (span_index, char_start)); 1291 - 1292 - // Record offset mapping for the syntax span content 1293 self.record_mapping(range.clone(), char_start..char_end); 1294 1295 // Count as child 1296 self.current_node_child_count += 1; 1297 - 1298 - // Emit the visible footnote reference (superscript number) 1299 - write!( 1300 - &mut self.writer, 1301 - "<sup class=\"footnote-reference\"><a href=\"#fn-{}\">{}</a></sup>", 1302 - name, number 1303 - )?; 1304 1305 // Update tracking 1306 self.last_char_offset = char_end; ··· 1348 } 1349 } 1350 Ok(()) 1351 } 1352 }
··· 10 pub mod syntax; 11 pub mod tags; 12 13 + use super::offset_map::OffsetMapping; 14 use crate::components::editor::writer::segmented::SegmentedWriter; 15 pub use embed::{EditorImageResolver, EmbedContentProvider, ImageResolver}; 16 use loro::LoroText; 17 use markdown_weaver::{Alignment, CowStr, Event, WeaverAttributes}; 18 use markdown_weaver_escape::{StrWrite, escape_html, escape_html_body_text_with_char_count}; 19 use std::collections::HashMap; 20 use std::fmt; 21 use std::ops::Range; 22 + pub use syntax::{SyntaxSpanInfo, SyntaxType}; 23 use weaver_common::EntryIndex; 24 25 /// Result of rendering with the EditorWriter. ··· 115 paragraph_ranges: Vec<(Range<usize>, Range<usize>)>, // (byte_range, char_range) 116 current_paragraph_start: Option<(usize, usize)>, // (byte_offset, char_offset) 117 list_depth: usize, // Track nesting depth to avoid paragraph boundary override inside lists 118 + in_footnote_def: bool, // Suppress inner paragraph boundaries in footnote definitions 119 120 // Syntax span tracking for conditional visibility - current paragraph 121 syntax_spans: Vec<SyntaxSpanInfo>, ··· 156 { 157 pub fn new(source: &'a str, source_text: &'a LoroText, events: I) -> Self { 158 Self::new_with_all_offsets(source, source_text, events, 0, 0, 0, 0) 159 } 160 161 /// Get the next paragraph ID that would be assigned (for tracking allocations). ··· 985 986 // Emit space for cursor positioning - this gives the browser somewhere 987 // to place the cursor when navigating to this line 988 + self.write("\u{200B}")?; 989 self.current_node_child_count += 1; 990 991 // Map the space to the newline position - cursor landing here means ··· 1039 self.current_node_child_count += 1; 1040 1041 // After <br>, emit plain zero-width space for cursor positioning 1042 + self.write("\u{200B}")?; 1043 1044 // Count the zero-width space text node as a child 1045 self.current_node_child_count += 1; ··· 1061 self.current_node_char_offset += 1; 1062 } 1063 1064 self.last_char_offset = char_start + spaces_char_len + 1; // +1 for \n 1065 } else { 1066 // Fallback: just <br> ··· 1103 self.write("<div class=\"toggle-block\"><hr /></div>\n")?; 1104 } 1105 FootnoteReference(name) => { 1106 + // Emit [^name] as styled (but NOT hidden) inline span 1107 let raw_text = &self.source[range.clone()]; 1108 let char_start = self.last_char_offset; 1109 let syntax_char_len = raw_text.chars().count(); 1110 let char_end = char_start + syntax_char_len; 1111 1112 + // Use footnote-ref class for styling, not md-syntax-inline (which hides) 1113 write!( 1114 &mut self.writer, 1115 + "<span class=\"footnote-ref\" data-char-start=\"{}\" data-char-end=\"{}\" data-footnote=\"{}\">", 1116 + char_start, char_end, name 1117 )?; 1118 escape_html(&mut self.writer, raw_text)?; 1119 self.write("</span>")?; 1120 1121 + // Record offset mapping 1122 self.record_mapping(range.clone(), char_start..char_end); 1123 1124 // Count as child 1125 self.current_node_child_count += 1; 1126 1127 // Update tracking 1128 self.last_char_offset = char_end; ··· 1170 } 1171 } 1172 Ok(()) 1173 + } 1174 + 1175 + pub fn new_with_all_offsets( 1176 + source: &'a str, 1177 + source_text: &'a LoroText, 1178 + events: I, 1179 + node_id_offset: usize, 1180 + syn_id_offset: usize, 1181 + char_offset_base: usize, 1182 + byte_offset_base: usize, 1183 + ) -> Self { 1184 + Self { 1185 + source, 1186 + source_text, 1187 + events, 1188 + writer: SegmentedWriter::new(), 1189 + last_byte_offset: byte_offset_base, 1190 + last_char_offset: char_offset_base, 1191 + end_newline: true, 1192 + in_non_writing_block: false, 1193 + table_state: TableState::Head, 1194 + table_alignments: vec![], 1195 + table_cell_index: 0, 1196 + numbers: HashMap::new(), 1197 + embed_provider: None, 1198 + image_resolver: None, 1199 + entry_index: None, 1200 + code_buffer: None, 1201 + code_buffer_byte_range: None, 1202 + code_buffer_char_range: None, 1203 + code_block_char_start: None, 1204 + code_block_opening_span_idx: None, 1205 + pending_blockquote_range: None, 1206 + render_tables_as_markdown: true, 1207 + table_start_offset: None, 1208 + offset_maps: Vec::new(), 1209 + node_id_prefix: None, 1210 + auto_increment_prefix: None, 1211 + static_prefix_override: None, 1212 + current_paragraph_index: 0, 1213 + next_node_id: node_id_offset, 1214 + current_node_id: None, 1215 + current_node_char_offset: 0, 1216 + current_node_child_count: 0, 1217 + utf16_checkpoints: vec![(0, 0)], 1218 + paragraph_ranges: Vec::new(), 1219 + current_paragraph_start: None, 1220 + list_depth: 0, 1221 + in_footnote_def: false, 1222 + syntax_spans: Vec::new(), 1223 + next_syn_id: syn_id_offset, 1224 + pending_inline_formats: Vec::new(), 1225 + ref_collector: weaver_common::RefCollector::new(), 1226 + offset_maps_by_para: Vec::new(), 1227 + syntax_spans_by_para: Vec::new(), 1228 + refs_by_para: Vec::new(), 1229 + pending_block_attrs: None, 1230 + active_wrapper: None, 1231 + weaver_block_buffer: String::new(), 1232 + weaver_block_char_start: None, 1233 + footnote_ref_spans: HashMap::new(), 1234 + current_footnote_def: None, 1235 + _phantom: std::marker::PhantomData, 1236 + } 1237 + } 1238 + 1239 + /// Add an embed content provider 1240 + pub fn with_embed_provider(mut self, provider: E) -> EditorWriter<'a, I, E, R> { 1241 + self.embed_provider = Some(provider); 1242 + self 1243 + } 1244 + 1245 + /// Add an image resolver for mapping markdown image URLs to CDN URLs 1246 + pub fn with_image_resolver<R2: ImageResolver>( 1247 + self, 1248 + resolver: R2, 1249 + ) -> EditorWriter<'a, I, E, R2> { 1250 + EditorWriter { 1251 + source: self.source, 1252 + source_text: self.source_text, 1253 + events: self.events, 1254 + writer: self.writer, 1255 + last_byte_offset: self.last_byte_offset, 1256 + last_char_offset: self.last_char_offset, 1257 + end_newline: self.end_newline, 1258 + in_non_writing_block: self.in_non_writing_block, 1259 + table_state: self.table_state, 1260 + table_alignments: self.table_alignments, 1261 + table_cell_index: self.table_cell_index, 1262 + numbers: self.numbers, 1263 + embed_provider: self.embed_provider, 1264 + image_resolver: Some(resolver), 1265 + entry_index: self.entry_index, 1266 + code_buffer: self.code_buffer, 1267 + code_buffer_byte_range: self.code_buffer_byte_range, 1268 + code_buffer_char_range: self.code_buffer_char_range, 1269 + code_block_char_start: self.code_block_char_start, 1270 + code_block_opening_span_idx: self.code_block_opening_span_idx, 1271 + pending_blockquote_range: self.pending_blockquote_range, 1272 + render_tables_as_markdown: self.render_tables_as_markdown, 1273 + table_start_offset: self.table_start_offset, 1274 + offset_maps: self.offset_maps, 1275 + node_id_prefix: self.node_id_prefix, 1276 + auto_increment_prefix: self.auto_increment_prefix, 1277 + static_prefix_override: self.static_prefix_override, 1278 + current_paragraph_index: self.current_paragraph_index, 1279 + next_node_id: self.next_node_id, 1280 + current_node_id: self.current_node_id, 1281 + current_node_char_offset: self.current_node_char_offset, 1282 + current_node_child_count: self.current_node_child_count, 1283 + utf16_checkpoints: self.utf16_checkpoints, 1284 + paragraph_ranges: self.paragraph_ranges, 1285 + current_paragraph_start: self.current_paragraph_start, 1286 + list_depth: self.list_depth, 1287 + in_footnote_def: self.in_footnote_def, 1288 + syntax_spans: self.syntax_spans, 1289 + next_syn_id: self.next_syn_id, 1290 + pending_inline_formats: self.pending_inline_formats, 1291 + ref_collector: self.ref_collector, 1292 + offset_maps_by_para: self.offset_maps_by_para, 1293 + syntax_spans_by_para: self.syntax_spans_by_para, 1294 + refs_by_para: self.refs_by_para, 1295 + pending_block_attrs: self.pending_block_attrs, 1296 + active_wrapper: self.active_wrapper, 1297 + weaver_block_buffer: self.weaver_block_buffer, 1298 + weaver_block_char_start: self.weaver_block_char_start, 1299 + footnote_ref_spans: self.footnote_ref_spans, 1300 + current_footnote_def: self.current_footnote_def, 1301 + _phantom: std::marker::PhantomData, 1302 + } 1303 + } 1304 + 1305 + /// Add an entry index for wikilink resolution feedback 1306 + pub fn with_entry_index(mut self, index: &'a EntryIndex) -> Self { 1307 + self.entry_index = Some(index); 1308 + self 1309 + } 1310 + 1311 + /// Set a prefix for node IDs (typically the paragraph ID). 1312 + /// This makes node IDs paragraph-scoped and stable across re-renders. 1313 + /// Use this for single-paragraph renders where the paragraph ID is known. 1314 + pub fn with_node_id_prefix(mut self, prefix: &str) -> Self { 1315 + self.node_id_prefix = Some(prefix.to_string()); 1316 + self.next_node_id = 0; // Reset counter since each paragraph is independent 1317 + self 1318 + } 1319 + 1320 + /// Enable auto-incrementing paragraph prefixes for multi-paragraph renders. 1321 + /// Each paragraph gets prefix "p-{N}" where N starts at `start_id` and increments. 1322 + /// Node IDs reset to 0 for each paragraph, giving "p-{N}-n0", "p-{N}-n1", etc. 1323 + pub fn with_auto_incrementing_prefix(mut self, start_id: usize) -> Self { 1324 + self.auto_increment_prefix = Some(start_id); 1325 + self.node_id_prefix = Some(format!("p-{}", start_id)); 1326 + self.next_node_id = 0; 1327 + self 1328 } 1329 }
+116 -71
crates/weaver-app/src/components/editor/writer/tags.rs
··· 16 impl<'a, I: Iterator<Item = (Event<'a>, Range<usize>)>, E: EmbedContentProvider, R: ImageResolver> 17 EditorWriter<'a, I, E, R> 18 { 19 pub(crate) fn start_tag( 20 &mut self, 21 tag: Tag<'_>, ··· 128 // HTML blocks get their own paragraph to try and corral them better 129 Tag::HtmlBlock => { 130 // Record paragraph start for boundary tracking 131 - // BUT skip if inside a list - list owns the paragraph boundary 132 - if self.list_depth == 0 { 133 self.current_paragraph_start = 134 Some((self.last_byte_offset, self.last_char_offset)); 135 } ··· 170 self.emit_wrapper_start()?; 171 172 // Record paragraph start for boundary tracking 173 - // BUT skip if inside a list - list owns the paragraph boundary 174 - if self.list_depth == 0 { 175 self.current_paragraph_start = 176 Some((self.last_byte_offset, self.last_char_offset)); 177 } 178 179 let node_id = self.gen_node_id(); 180 if self.end_newline { 181 - write!(&mut self.writer, "<p id=\"{}\">", node_id)?; 182 } else { 183 - write!(&mut self.writer, "\n<p id=\"{}\">", node_id)?; 184 } 185 self.begin_node(node_id.clone()); 186 ··· 244 // Generate node ID for offset tracking 245 let node_id = self.gen_node_id(); 246 247 self.write("<")?; 248 write!(&mut self.writer, "{}", level)?; 249 ··· 267 } 268 self.write("\"")?; 269 } 270 for (attr, value) in attrs { 271 self.write(" ")?; 272 escape_html(&mut self.writer, &attr)?; ··· 868 Ok(()) 869 } 870 Tag::FootnoteDefinition(name) => { 871 - // Emit the [^name]: prefix as a hideable syntax span 872 - // The source should have "[^name]: " at the start 873 - let prefix = format!("[^{}]: ", name); 874 - let char_start = self.last_char_offset; 875 - let prefix_char_len = prefix.chars().count(); 876 - let char_end = char_start + prefix_char_len; 877 - let syn_id = self.gen_syn_id(); 878 879 if !self.end_newline { 880 self.write("\n")?; 881 } 882 883 write!( 884 &mut self.writer, 885 - "<span class=\"md-syntax-block\" data-syn-id=\"{}\" data-char-start=\"{}\" data-char-end=\"{}\" spellcheck=\"false\">", 886 - syn_id, char_start, char_end 887 )?; 888 - escape_html(&mut self.writer, &prefix)?; 889 self.write("</span>")?; 890 891 - // Track this span for linking with the footnote reference 892 - let def_span_index = self.syntax_spans.len(); 893 - self.syntax_spans.push(SyntaxSpanInfo { 894 - syn_id, 895 - char_range: char_start..char_end, 896 - syntax_type: SyntaxType::Block, 897 - formatted_range: None, // Set at FootnoteDefinition end 898 - }); 899 - 900 - // Store the definition info for linking at end 901 - self.current_footnote_def = Some((name.to_string(), def_span_index, char_start)); 902 903 - // Record offset mapping for the syntax span 904 self.record_mapping( 905 - range.start..range.start + prefix.len(), 906 char_start..char_end, 907 ); 908 909 // Update tracking for the prefix 910 self.last_char_offset = char_end; 911 - self.last_byte_offset = range.start + prefix.len(); 912 - 913 - // Emit the definition container 914 - write!( 915 - &mut self.writer, 916 - "<div class=\"footnote-definition\" id=\"fn-{}\">", 917 - name 918 - )?; 919 - 920 - // Get/create footnote number for the label 921 - let len = self.numbers.len() + 1; 922 - let number = *self.numbers.entry(name.to_string()).or_insert(len); 923 - write!( 924 - &mut self.writer, 925 - "<sup class=\"footnote-definition-label\">{}</sup>", 926 - number 927 - )?; 928 929 Ok(()) 930 } ··· 946 let result = match tag { 947 TagEnd::HtmlBlock => { 948 // Capture paragraph boundary info BEFORE writing closing HTML 949 - // Skip if inside a list - list owns the paragraph boundary 950 - let para_boundary = if self.list_depth == 0 { 951 self.current_paragraph_start 952 .take() 953 .map(|(byte_start, char_start)| { ··· 972 } 973 TagEnd::Paragraph(_) => { 974 // Capture paragraph boundary info BEFORE writing closing HTML 975 - // Skip if inside a list - list owns the paragraph boundary 976 - let para_boundary = if self.list_depth == 0 { 977 self.current_paragraph_start 978 .take() 979 .map(|(byte_start, char_start)| { ··· 1418 Ok(()) 1419 } 1420 TagEnd::FootnoteDefinition => { 1421 self.write("</div>\n")?; 1422 1423 - // Link the footnote definition span with its reference span 1424 - if let Some((name, def_span_index, _def_char_start)) = 1425 - self.current_footnote_def.take() 1426 - { 1427 - let def_char_end = self.last_char_offset; 1428 1429 - // Look up the reference span 1430 - if let Some(&(ref_span_index, ref_char_start)) = 1431 - self.footnote_ref_spans.get(&name) 1432 - { 1433 - // Create formatted_range spanning from ref start to def end 1434 - let formatted_range = ref_char_start..def_char_end; 1435 - 1436 - // Update both spans with the same formatted_range 1437 - // so they show/hide together based on cursor proximity 1438 - if let Some(ref_span) = self.syntax_spans.get_mut(ref_span_index) { 1439 - ref_span.formatted_range = Some(formatted_range.clone()); 1440 - } 1441 - if let Some(def_span) = self.syntax_spans.get_mut(def_span_index) { 1442 - def_span.formatted_range = Some(formatted_range); 1443 - } 1444 - } 1445 } 1446 1447 Ok(())
··· 16 impl<'a, I: Iterator<Item = (Event<'a>, Range<usize>)>, E: EmbedContentProvider, R: ImageResolver> 17 EditorWriter<'a, I, E, R> 18 { 19 + /// Detect text direction by scanning source from a byte offset. 20 + /// Looks for the first strong directional character. 21 + /// Returns Some("rtl") for RTL scripts, Some("ltr") for LTR, None if no strong char found. 22 + fn detect_paragraph_direction(&self, start_byte: usize) -> Option<&'static str> { 23 + if start_byte >= self.source.len() { 24 + return None; 25 + } 26 + 27 + // Scan from start_byte through the source looking for first strong directional char 28 + let text = &self.source[start_byte..]; 29 + weaver_renderer::utils::detect_text_direction(text) 30 + } 31 + 32 pub(crate) fn start_tag( 33 &mut self, 34 tag: Tag<'_>, ··· 141 // HTML blocks get their own paragraph to try and corral them better 142 Tag::HtmlBlock => { 143 // Record paragraph start for boundary tracking 144 + // Skip if inside a list or footnote def - they own their paragraph boundary 145 + if self.list_depth == 0 && !self.in_footnote_def { 146 self.current_paragraph_start = 147 Some((self.last_byte_offset, self.last_char_offset)); 148 } ··· 183 self.emit_wrapper_start()?; 184 185 // Record paragraph start for boundary tracking 186 + // Skip if inside a list or footnote def - they own their paragraph boundary 187 + if self.list_depth == 0 && !self.in_footnote_def { 188 self.current_paragraph_start = 189 Some((self.last_byte_offset, self.last_char_offset)); 190 } 191 192 let node_id = self.gen_node_id(); 193 + 194 + // Detect text direction for this paragraph 195 + let dir = self.detect_paragraph_direction(self.last_byte_offset); 196 + 197 if self.end_newline { 198 + if let Some(dir_value) = dir { 199 + write!(&mut self.writer, "<p id=\"{}\" dir=\"{}\">", node_id, dir_value)?; 200 + } else { 201 + write!(&mut self.writer, "<p id=\"{}\">", node_id)?; 202 + } 203 } else { 204 + if let Some(dir_value) = dir { 205 + write!(&mut self.writer, "\n<p id=\"{}\" dir=\"{}\">", node_id, dir_value)?; 206 + } else { 207 + write!(&mut self.writer, "\n<p id=\"{}\">", node_id)?; 208 + } 209 } 210 self.begin_node(node_id.clone()); 211 ··· 269 // Generate node ID for offset tracking 270 let node_id = self.gen_node_id(); 271 272 + // Detect text direction for this heading 273 + let dir = self.detect_paragraph_direction(self.last_byte_offset); 274 + 275 self.write("<")?; 276 write!(&mut self.writer, "{}", level)?; 277 ··· 295 } 296 self.write("\"")?; 297 } 298 + 299 + // Add dir attribute if text direction was detected 300 + if let Some(dir_value) = dir { 301 + self.write(" dir=\"")?; 302 + self.write(dir_value)?; 303 + self.write("\"")?; 304 + } 305 + 306 for (attr, value) in attrs { 307 self.write(" ")?; 308 escape_html(&mut self.writer, &attr)?; ··· 904 Ok(()) 905 } 906 Tag::FootnoteDefinition(name) => { 907 + // Track as paragraph-level block for incremental rendering 908 + self.current_paragraph_start = Some((self.last_byte_offset, self.last_char_offset)); 909 + // Suppress inner paragraph boundaries (footnote def owns its paragraph) 910 + self.in_footnote_def = true; 911 912 if !self.end_newline { 913 self.write("\n")?; 914 } 915 916 + // Generate node ID for cursor tracking 917 + let node_id = self.gen_node_id(); 918 + 919 + // Emit wrapper div with NEW class (not footnote-definition which has order:9999) 920 + // This keeps footnotes in-place instead of reordering to bottom 921 write!( 922 &mut self.writer, 923 + "<div class=\"footnote-def-editor\" data-node-id=\"{}\">", 924 + node_id 925 + )?; 926 + 927 + // Begin node tracking BEFORE emitting prefix 928 + self.begin_node(node_id.clone()); 929 + 930 + // Map the start position (before any content) 931 + let fn_start_char = self.last_char_offset; 932 + let mapping = OffsetMapping { 933 + byte_range: range.start..range.start, 934 + char_range: fn_start_char..fn_start_char, 935 + node_id, 936 + char_offset_in_node: 0, 937 + child_index: Some(0), 938 + utf16_len: 0, 939 + }; 940 + self.offset_maps.push(mapping); 941 + 942 + // Extract ACTUAL prefix from source (not constructed string) 943 + // This ensures byte offsets match reality 944 + let raw_text = &self.source[range.clone()]; 945 + let prefix_end = raw_text 946 + .find("]:") 947 + .map(|p| { 948 + // Include ]: and any single trailing space 949 + let after_colon = p + 2; 950 + if raw_text.get(after_colon..after_colon + 1) == Some(" ") { 951 + after_colon + 1 952 + } else { 953 + after_colon 954 + } 955 + }) 956 + .unwrap_or(0); 957 + let prefix = &raw_text[..prefix_end]; 958 + let prefix_byte_len = prefix.len(); 959 + let prefix_char_len = prefix.chars().count(); 960 + 961 + let char_start = self.last_char_offset; 962 + let char_end = char_start + prefix_char_len; 963 + 964 + write!( 965 + &mut self.writer, 966 + "<span class=\"footnote-def-syntax\" data-char-start=\"{}\" data-char-end=\"{}\">", 967 + char_start, char_end 968 )?; 969 + escape_html(&mut self.writer, prefix)?; 970 self.write("</span>")?; 971 972 + // Store the definition info (no longer tracking syntax spans for hide/show) 973 + self.current_footnote_def = Some((name.to_string(), 0, char_start)); 974 975 + // Record offset mapping for the prefix 976 self.record_mapping( 977 + range.start..range.start + prefix_byte_len, 978 char_start..char_end, 979 ); 980 981 // Update tracking for the prefix 982 self.last_char_offset = char_end; 983 + self.last_byte_offset = range.start + prefix_byte_len; 984 985 Ok(()) 986 } ··· 1002 let result = match tag { 1003 TagEnd::HtmlBlock => { 1004 // Capture paragraph boundary info BEFORE writing closing HTML 1005 + // Skip if inside a list or footnote def - they own their paragraph boundary 1006 + let para_boundary = if self.list_depth == 0 && !self.in_footnote_def { 1007 self.current_paragraph_start 1008 .take() 1009 .map(|(byte_start, char_start)| { ··· 1028 } 1029 TagEnd::Paragraph(_) => { 1030 // Capture paragraph boundary info BEFORE writing closing HTML 1031 + // Skip if inside a list or footnote def - they own their paragraph boundary 1032 + let para_boundary = if self.list_depth == 0 && !self.in_footnote_def { 1033 self.current_paragraph_start 1034 .take() 1035 .map(|(byte_start, char_start)| { ··· 1474 Ok(()) 1475 } 1476 TagEnd::FootnoteDefinition => { 1477 + // End node tracking (inner paragraphs may have already cleared it) 1478 + self.end_node(); 1479 self.write("</div>\n")?; 1480 1481 + // Clear footnote tracking 1482 + self.current_footnote_def.take(); 1483 + self.in_footnote_def = false; 1484 1485 + // Finalize paragraph boundary for incremental rendering 1486 + if let Some((byte_start, char_start)) = self.current_paragraph_start.take() { 1487 + let byte_range = byte_start..self.last_byte_offset; 1488 + let char_range = char_start..self.last_char_offset; 1489 + self.finalize_paragraph(byte_range, char_range); 1490 } 1491 1492 Ok(())
-1
crates/weaver-app/src/lib.rs
··· 1 //! Weaver App library. 2 - 3 #[allow(unused)] 4 use dioxus::{CapturedError, prelude::*}; 5
··· 1 //! Weaver App library. 2 #[allow(unused)] 3 use dioxus::{CapturedError, prelude::*}; 4
+3 -2
crates/weaver-app/src/main.rs
··· 29 use tracing_subscriber::layer::SubscriberExt; 30 31 let console_level = if cfg!(debug_assertions) { 32 - Level::DEBUG 33 } else { 34 Level::INFO 35 }; ··· 41 ); 42 43 // Filter out noisy crates 44 let filter = EnvFilter::new( 45 - "debug,loro_internal=warn,jacquard_identity=info,jacquard_common=info,iroh=info", 46 ); 47 48 let reg = Registry::default()
··· 29 use tracing_subscriber::layer::SubscriberExt; 30 31 let console_level = if cfg!(debug_assertions) { 32 + Level::TRACE 33 } else { 34 Level::INFO 35 }; ··· 41 ); 42 43 // Filter out noisy crates 44 + // Use weaver_app=trace for detailed editor debugging 45 let filter = EnvFilter::new( 46 + "debug,weaver_app=trace,loro_internal=warn,jacquard_identity=info,jacquard_common=info,iroh=info", 47 ); 48 49 let reg = Registry::default()
+5 -3
crates/weaver-common/src/agent.rs
··· 17 use jacquard::prelude::*; 18 use jacquard::smol_str::SmolStr; 19 use jacquard::types::blob::{BlobRef, MimeType}; 20 - use jacquard::types::string::{AtUri, Did, RecordKey, Rkey}; 21 #[allow(unused_imports)] 22 use jacquard::types::tid::Tid; 23 use jacquard::types::uri::Uri; ··· 138 async move { 139 use weaver_api::sh_weaver::notebook::book::Book; 140 141 - let at_uri = AtUri::new(uri) 142 - .map_err(|e| WeaverError::InvalidNotebook(format!("Invalid notebook URI: {}", e)))?; 143 144 let response = match self.get_record::<Book>(&at_uri).await { 145 Ok(r) => r, ··· 352 e.content = entry.content.clone(); 353 e.embeds = entry.embeds.clone(); 354 e.tags = entry.tags.clone(); 355 }) 356 .await?; 357 let updated_ref = StrongRef::new()
··· 17 use jacquard::prelude::*; 18 use jacquard::smol_str::SmolStr; 19 use jacquard::types::blob::{BlobRef, MimeType}; 20 + use jacquard::types::string::{AtUri, Datetime, Did, RecordKey, Rkey}; 21 #[allow(unused_imports)] 22 use jacquard::types::tid::Tid; 23 use jacquard::types::uri::Uri; ··· 138 async move { 139 use weaver_api::sh_weaver::notebook::book::Book; 140 141 + let at_uri = AtUri::new(uri).map_err(|e| { 142 + WeaverError::InvalidNotebook(format!("Invalid notebook URI: {}", e)) 143 + })?; 144 145 let response = match self.get_record::<Book>(&at_uri).await { 146 Ok(r) => r, ··· 353 e.content = entry.content.clone(); 354 e.embeds = entry.embeds.clone(); 355 e.tags = entry.tags.clone(); 356 + e.updated_at = Some(Datetime::now()); 357 }) 358 .await?; 359 let updated_ref = StrongRef::new()
+12 -9
crates/weaver-index/src/clickhouse/queries/edit.rs
··· 208 /// 209 /// Compares edit_heads to draft_titles to find drafts where the current 210 /// head doesn't match the head used for title extraction. 211 - pub async fn get_stale_draft_titles(&self, limit: i64) -> Result<Vec<StaleDraftRow>, IndexError> { 212 // Join drafts -> edit_heads (for current head) -> draft_titles (to check staleness) 213 // edit_heads uses resource_type='draft' and resource_did/resource_rkey to link 214 let query = r#" 215 SELECT 216 - d.did, 217 - d.rkey, 218 - h.head_did, 219 - h.head_rkey, 220 - h.head_cid, 221 - h.root_did, 222 - h.root_rkey, 223 - h.root_cid 224 FROM drafts d FINAL 225 INNER JOIN edit_heads h FINAL 226 ON h.resource_did = d.did
··· 208 /// 209 /// Compares edit_heads to draft_titles to find drafts where the current 210 /// head doesn't match the head used for title extraction. 211 + pub async fn get_stale_draft_titles( 212 + &self, 213 + limit: i64, 214 + ) -> Result<Vec<StaleDraftRow>, IndexError> { 215 // Join drafts -> edit_heads (for current head) -> draft_titles (to check staleness) 216 // edit_heads uses resource_type='draft' and resource_did/resource_rkey to link 217 let query = r#" 218 SELECT 219 + d.did as did, 220 + d.rkey as rkey, 221 + h.head_did as head_did, 222 + h.head_rkey as head_rkey, 223 + h.head_cid as head_cid, 224 + h.root_did as root_did, 225 + h.root_rkey as root_rkey, 226 + h.root_cid as root_cid 227 FROM drafts d FINAL 228 INNER JOIN edit_heads h FINAL 229 ON h.resource_did = d.did
+1
crates/weaver-renderer/Cargo.toml
··· 19 tracing.workspace = true 20 miette.workspace = true 21 unicode-normalization = "0.1.24" 22 yaml-rust2 = { version = "0.10.2" } 23 bitflags = "2.9.1" 24 dashmap = "6.1.0"
··· 19 tracing.workspace = true 20 miette.workspace = true 21 unicode-normalization = "0.1.24" 22 + unicode-bidi = "0.3" 23 yaml-rust2 = { version = "0.10.2" } 24 bitflags = "2.9.1" 25 dashmap = "6.1.0"
+2 -2
crates/weaver-renderer/src/atproto/snapshots/weaver_renderer__atproto__tests__blockquote_rendering.snap
··· 3 expression: output 4 --- 5 <blockquote> 6 - <p>This is a quote</p> 7 - <p>With multiple lines</p> 8 </blockquote>
··· 3 expression: output 4 --- 5 <blockquote> 6 + <p dir="ltr">This is a quote</p> 7 + <p dir="ltr">With multiple lines</p> 8 </blockquote>
+2 -2
crates/weaver-renderer/src/atproto/snapshots/weaver_renderer__atproto__tests__footnote_in_blockquote.snap
··· 3 expression: output 4 --- 5 <blockquote> 6 - <p>Quote with footnote<sup class="footnote-reference"><a href="#q">1</a></sup>.</p> 7 </blockquote> 8 <div class="footnote-definition" id="q"><sup class="footnote-definition-label">1</sup> 9 - <p>Footnote for quote.</p> 10 </div>
··· 3 expression: output 4 --- 5 <blockquote> 6 + <p dir="ltr">Quote with footnote<sup class="footnote-reference"><a href="#q">1</a></sup>.</p> 7 </blockquote> 8 <div class="footnote-definition" id="q"><sup class="footnote-definition-label">1</sup> 9 + <p dir="ltr">Footnote for quote.</p> 10 </div>
+3 -3
crates/weaver-renderer/src/atproto/snapshots/weaver_renderer__atproto__tests__footnote_multiple.snap
··· 2 source: crates/weaver-renderer/src/atproto/tests.rs 3 expression: output 4 --- 5 - <p>First<sup class="footnote-reference"><a href="#1">1</a></sup> and second<sup class="footnote-reference"><a href="#2">2</a></sup> footnotes.</p> 6 <div class="footnote-definition" id="1"><sup class="footnote-definition-label">1</sup> 7 - <p>First note.</p> 8 </div> 9 <div class="footnote-definition" id="2"><sup class="footnote-definition-label">2</sup> 10 - <p>Second note.</p> 11 </div>
··· 2 source: crates/weaver-renderer/src/atproto/tests.rs 3 expression: output 4 --- 5 + <p dir="ltr">First<sup class="footnote-reference"><a href="#1">1</a></sup> and second<sup class="footnote-reference"><a href="#2">2</a></sup> footnotes.</p> 6 <div class="footnote-definition" id="1"><sup class="footnote-definition-label">1</sup> 7 + <p dir="ltr">First note.</p> 8 </div> 9 <div class="footnote-definition" id="2"><sup class="footnote-definition-label">2</sup> 10 + <p dir="ltr">Second note.</p> 11 </div>
+1 -1
crates/weaver-renderer/src/atproto/snapshots/weaver_renderer__atproto__tests__footnote_named.snap
··· 2 source: crates/weaver-renderer/src/atproto/tests.rs 3 expression: output 4 --- 5 - <p>Reference<label for="sn-1" class="margin-toggle sidenote-number"></label><input type="checkbox" id="sn-1" class="margin-toggle"/><span class="sidenote">Named footnote content.</span>.</p>
··· 2 source: crates/weaver-renderer/src/atproto/tests.rs 3 expression: output 4 --- 5 + <p dir="ltr">Reference<label for="sn-1" class="sidenote-number"></label><input type="checkbox" id="sn-1" class="margin-toggle"/><span class="sidenote">Named footnote content.</span>.</p>
+1 -1
crates/weaver-renderer/src/atproto/snapshots/weaver_renderer__atproto__tests__footnote_sidenote_inline.snap
··· 2 source: crates/weaver-renderer/src/atproto/tests.rs 3 expression: output 4 --- 5 - <p>Here is text<label for="sn-1" class="margin-toggle sidenote-number"></label><input type="checkbox" id="sn-1" class="margin-toggle"/><span class="sidenote">Sidenote content.</span></p>
··· 2 source: crates/weaver-renderer/src/atproto/tests.rs 3 expression: output 4 --- 5 + <p dir="ltr">Here is text<label for="sn-1" class="sidenote-number"></label><input type="checkbox" id="sn-1" class="margin-toggle"/><span class="sidenote">Sidenote content.</span></p>
+1 -1
crates/weaver-renderer/src/atproto/snapshots/weaver_renderer__atproto__tests__footnote_traditional.snap
··· 2 source: crates/weaver-renderer/src/atproto/tests.rs 3 expression: output 4 --- 5 - <p>Here is some text<label for="sn-1" class="margin-toggle sidenote-number"></label><input type="checkbox" id="sn-1" class="margin-toggle"/><span class="sidenote">This is the footnote definition.</span>.</p>
··· 2 source: crates/weaver-renderer/src/atproto/tests.rs 3 expression: output 4 --- 5 + <p dir="ltr">Here is some text<label for="sn-1" class="sidenote-number"></label><input type="checkbox" id="sn-1" class="margin-toggle"/><span class="sidenote">This is the footnote definition.</span>.</p>
+1 -1
crates/weaver-renderer/src/atproto/snapshots/weaver_renderer__atproto__tests__footnote_with_inline_formatting.snap
··· 2 source: crates/weaver-renderer/src/atproto/tests.rs 3 expression: output 4 --- 5 - <p>Text with footnote<label for="sn-1" class="margin-toggle sidenote-number"></label><input type="checkbox" id="sn-1" class="margin-toggle"/><span class="sidenote">Note with <strong>bold</strong> and <em>italic</em>.</span>.</p>
··· 2 source: crates/weaver-renderer/src/atproto/tests.rs 3 expression: output 4 --- 5 + <p dir="ltr">Text with footnote<label for="sn-1" class="sidenote-number"></label><input type="checkbox" id="sn-1" class="margin-toggle"/><span class="sidenote">Note with <strong>bold</strong> and <em>italic</em>.</span>.</p>
+2 -2
crates/weaver-renderer/src/atproto/snapshots/weaver_renderer__atproto__tests__math_rendering.snap
··· 2 source: crates/weaver-renderer/src/atproto/tests.rs 3 expression: output 4 --- 5 - <p>Inline <span class="math math-inline"><math display="inline"><msup><mi>x</mi><mn>2</mn></msup></math></span> and display:</p> 6 - <p><span class="math math-display"><math display="block"><mi>y</mi><mo>=</mo><mi>m</mi><mi>x</mi><mo>+</mo><mi>b</mi></math></span></p>
··· 2 source: crates/weaver-renderer/src/atproto/tests.rs 3 expression: output 4 --- 5 + <p dir="ltr">Inline <span class="math math-inline"><math display="inline"><msup><mi>x</mi><mn>2</mn></msup></math></span> and display:</p> 6 + <span class="math math-display"><math display="block"><mi>y</mi><mo>=</mo><mi>m</mi><mi>x</mi><mo>+</mo><mi>b</mi></math></span><p></p>
+2 -2
crates/weaver-renderer/src/atproto/snapshots/weaver_renderer__atproto__tests__paragraph_rendering.snap
··· 2 source: crates/weaver-renderer/src/atproto/tests.rs 3 expression: output 4 --- 5 - <p>This is a paragraph.</p> 6 - <p>This is another paragraph.</p>
··· 2 source: crates/weaver-renderer/src/atproto/tests.rs 3 expression: output 4 --- 5 + <p dir="ltr">This is a paragraph.</p> 6 + <p dir="ltr">This is another paragraph.</p>
+1 -1
crates/weaver-renderer/src/atproto/snapshots/weaver_renderer__atproto__tests__weaver_block_aside_class.snap
··· 3 expression: output 4 --- 5 <aside> 6 - <p>This paragraph should be in an aside.</p> 7 </aside>
··· 3 expression: output 4 --- 5 <aside> 6 + <p dir="ltr">This paragraph should be in an aside.</p> 7 </aside>
+1 -1
crates/weaver-renderer/src/atproto/snapshots/weaver_renderer__atproto__tests__weaver_block_before_blockquote.snap
··· 4 --- 5 <aside> 6 <blockquote> 7 - <p>This blockquote is in an aside.</p> 8 </aside> 9 </blockquote>
··· 4 --- 5 <aside> 6 <blockquote> 7 + <p dir="ltr">This blockquote is in an aside.</p> 8 </aside> 9 </blockquote>
+1 -1
crates/weaver-renderer/src/atproto/snapshots/weaver_renderer__atproto__tests__weaver_block_before_heading.snap
··· 4 --- 5 <aside> 6 <h2>Heading in aside</h2> 7 - <p>Paragraph also in aside.</p> 8 </aside>
··· 4 --- 5 <aside> 6 <h2>Heading in aside</h2> 7 + <p dir="ltr">Paragraph also in aside.</p> 8 </aside>
+1 -1
crates/weaver-renderer/src/atproto/snapshots/weaver_renderer__atproto__tests__weaver_block_custom_attributes.snap
··· 3 expression: output 4 --- 5 <div class="foo" width="300px" data-test="value"> 6 - <p>Paragraph with class and attributes.</p> 7 </div>
··· 3 expression: output 4 --- 5 <div class="foo" width="300px" data-test="value"> 6 + <p dir="ltr">Paragraph with class and attributes.</p> 7 </div>
+1 -1
crates/weaver-renderer/src/atproto/snapshots/weaver_renderer__atproto__tests__weaver_block_custom_class.snap
··· 3 expression: output 4 --- 5 <div class="highlight"> 6 - <p>This paragraph has a custom class.</p> 7 </div>
··· 3 expression: output 4 --- 5 <div class="highlight"> 6 + <p dir="ltr">This paragraph has a custom class.</p> 7 </div>
+1 -1
crates/weaver-renderer/src/atproto/snapshots/weaver_renderer__atproto__tests__weaver_block_multiple_classes.snap
··· 3 expression: output 4 --- 5 <aside class="highlight important"> 6 - <p>Multiple classes applied.</p> 7 </aside>
··· 3 expression: output 4 --- 5 <aside class="highlight important"> 6 + <p dir="ltr">Multiple classes applied.</p> 7 </aside>
+2 -2
crates/weaver-renderer/src/atproto/snapshots/weaver_renderer__atproto__tests__weaver_block_no_effect_on_following.snap
··· 3 expression: output 4 --- 5 <aside> 6 - <p>First paragraph in aside.</p> 7 </aside> 8 - <p>Second paragraph NOT in aside.</p>
··· 3 expression: output 4 --- 5 <aside> 6 + <p dir="ltr">First paragraph in aside.</p> 7 </aside> 8 + <p dir="ltr">Second paragraph NOT in aside.</p>
+1 -1
crates/weaver-renderer/src/atproto/snapshots/weaver_renderer__atproto__tests__weaver_block_with_footnote.snap
··· 3 expression: output 4 --- 5 <aside> 6 - <p>Aside with a footnote<label for="sn-1" class="margin-toggle sidenote-number"></label><input type="checkbox" id="sn-1" class="margin-toggle"/><span class="sidenote">Footnote in aside context.</span>.</p> 7 </aside>
··· 3 expression: output 4 --- 5 <aside> 6 + <p dir="ltr">Aside with a footnote<label for="sn-1" class="sidenote-number"></label><input type="checkbox" id="sn-1" class="margin-toggle"/><span class="sidenote">Footnote in aside context.</span>.</p> 7 </aside>
+33 -4
crates/weaver-renderer/src/atproto/writer.rs
··· 92 in_sidenote: bool, 93 /// Whether we're deferring paragraph close for sidenote handling 94 defer_paragraph_close: bool, 95 96 _phantom: std::marker::PhantomData<&'a ()>, 97 } ··· 126 pending_footnote_content: self.pending_footnote_content, 127 in_sidenote: self.in_sidenote, 128 defer_paragraph_close: self.defer_paragraph_close, 129 _phantom: std::marker::PhantomData, 130 } 131 } ··· 153 pending_footnote_content: String::new(), 154 in_sidenote: false, 155 defer_paragraph_close: false, 156 _phantom: std::marker::PhantomData, 157 } 158 } ··· 191 /// Close deferred paragraph if we're in that state. 192 /// Called when a non-paragraph block element starts. 193 fn close_deferred_paragraph(&mut self) -> Result<(), W::Error> { 194 if self.defer_paragraph_close { 195 // Flush pending footnote as traditional before closing 196 self.flush_pending_footnote()?; ··· 397 // Buffer text while waiting to see if footnote def follows 398 self.pending_footnote_content.push_str(&text); 399 } else if !self.in_non_writing_block { 400 escape_html_body_text(&mut self.writer, &text)?; 401 self.end_newline = text.ends_with('\n'); 402 } ··· 495 } else { 496 self.flush_pending_footnote()?; 497 self.emit_wrapper_start()?; 498 - if self.end_newline { 499 - self.write("<p>") 500 } else { 501 - self.write("\n<p>") 502 - } 503 } 504 } 505 Tag::Heading { ··· 857 self.defer_paragraph_close = false; 858 Ok(()) 859 } else { 860 self.write("</p>\n")?; 861 self.close_wrapper() 862 }
··· 92 in_sidenote: bool, 93 /// Whether we're deferring paragraph close for sidenote handling 94 defer_paragraph_close: bool, 95 + /// Buffered paragraph opening tag (without closing `>`) for dir attribute emission 96 + pending_paragraph_open: Option<String>, 97 98 _phantom: std::marker::PhantomData<&'a ()>, 99 } ··· 128 pending_footnote_content: self.pending_footnote_content, 129 in_sidenote: self.in_sidenote, 130 defer_paragraph_close: self.defer_paragraph_close, 131 + pending_paragraph_open: self.pending_paragraph_open, 132 _phantom: std::marker::PhantomData, 133 } 134 } ··· 156 pending_footnote_content: String::new(), 157 in_sidenote: false, 158 defer_paragraph_close: false, 159 + pending_paragraph_open: None, 160 _phantom: std::marker::PhantomData, 161 } 162 } ··· 195 /// Close deferred paragraph if we're in that state. 196 /// Called when a non-paragraph block element starts. 197 fn close_deferred_paragraph(&mut self) -> Result<(), W::Error> { 198 + // Also flush any pending paragraph open (shouldn't happen in normal flow, but be defensive) 199 + if let Some(opening) = self.pending_paragraph_open.take() { 200 + self.write(&opening)?; 201 + self.write(">")?; 202 + } 203 if self.defer_paragraph_close { 204 // Flush pending footnote as traditional before closing 205 self.flush_pending_footnote()?; ··· 406 // Buffer text while waiting to see if footnote def follows 407 self.pending_footnote_content.push_str(&text); 408 } else if !self.in_non_writing_block { 409 + // Flush pending paragraph with dir attribute if needed 410 + if let Some(opening) = self.pending_paragraph_open.take() { 411 + if let Some(dir) = crate::utils::detect_text_direction(&text) { 412 + self.write(&opening)?; 413 + self.write(" dir=\"")?; 414 + self.write(dir)?; 415 + self.write("\">")?; 416 + } else { 417 + self.write(&opening)?; 418 + self.write(">")?; 419 + } 420 + } 421 escape_html_body_text(&mut self.writer, &text)?; 422 self.end_newline = text.ends_with('\n'); 423 } ··· 516 } else { 517 self.flush_pending_footnote()?; 518 self.emit_wrapper_start()?; 519 + // Buffer paragraph opening for dir attribute detection 520 + let opening = if self.end_newline { 521 + String::from("<p") 522 } else { 523 + String::from("\n<p") 524 + }; 525 + self.pending_paragraph_open = Some(opening); 526 + Ok(()) 527 } 528 } 529 Tag::Heading { ··· 881 self.defer_paragraph_close = false; 882 Ok(()) 883 } else { 884 + // Flush any pending paragraph open (for empty paragraphs) 885 + if let Some(opening) = self.pending_paragraph_open.take() { 886 + self.write(&opening)?; 887 + self.write(">")?; 888 + } 889 self.write("</p>\n")?; 890 self.close_wrapper() 891 }
+27 -4
crates/weaver-renderer/src/base_html.rs
··· 38 table_alignments: Vec<Alignment>, 39 table_cell_index: usize, 40 numbers: HashMap<CowStr<'a>, usize>, 41 } 42 43 impl<'a, I, W> HtmlWriter<'a, I, W> ··· 55 table_alignments: vec![], 56 table_cell_index: 0, 57 numbers: HashMap::new(), 58 } 59 } 60 ··· 87 } 88 Text(text) => { 89 if !self.in_non_writing_block { 90 escape_html_body_text(&mut self.writer, &text)?; 91 self.end_newline = text.ends_with('\n'); 92 } ··· 158 match tag { 159 Tag::HtmlBlock => Ok(()), 160 Tag::Paragraph(_) => { 161 - if self.end_newline { 162 - self.write("<p>") 163 } else { 164 - self.write("\n<p>") 165 - } 166 } 167 Tag::Heading { 168 level, ··· 458 match tag { 459 TagEnd::HtmlBlock => {} 460 TagEnd::Paragraph(_) => { 461 self.write("</p>\n")?; 462 } 463 TagEnd::Heading(level) => {
··· 38 table_alignments: Vec<Alignment>, 39 table_cell_index: usize, 40 numbers: HashMap<CowStr<'a>, usize>, 41 + /// Buffered paragraph opening tag (without closing `>`) for dir attribute emission 42 + pending_paragraph_open: Option<String>, 43 } 44 45 impl<'a, I, W> HtmlWriter<'a, I, W> ··· 57 table_alignments: vec![], 58 table_cell_index: 0, 59 numbers: HashMap::new(), 60 + pending_paragraph_open: None, 61 } 62 } 63 ··· 90 } 91 Text(text) => { 92 if !self.in_non_writing_block { 93 + // Flush pending paragraph with dir attribute if needed 94 + if let Some(opening) = self.pending_paragraph_open.take() { 95 + if let Some(dir) = crate::utils::detect_text_direction(&text) { 96 + self.write(&opening)?; 97 + self.write(" dir=\"")?; 98 + self.write(dir)?; 99 + self.write("\">")?; 100 + } else { 101 + self.write(&opening)?; 102 + self.write(">")?; 103 + } 104 + } 105 escape_html_body_text(&mut self.writer, &text)?; 106 self.end_newline = text.ends_with('\n'); 107 } ··· 173 match tag { 174 Tag::HtmlBlock => Ok(()), 175 Tag::Paragraph(_) => { 176 + // Buffer paragraph opening for dir attribute detection 177 + let opening = if self.end_newline { 178 + String::from("<p") 179 } else { 180 + String::from("\n<p") 181 + }; 182 + self.pending_paragraph_open = Some(opening); 183 + Ok(()) 184 } 185 Tag::Heading { 186 level, ··· 476 match tag { 477 TagEnd::HtmlBlock => {} 478 TagEnd::Paragraph(_) => { 479 + // Flush any pending paragraph open (for empty paragraphs) 480 + if let Some(opening) = self.pending_paragraph_open.take() { 481 + self.write(&opening)?; 482 + self.write(">")?; 483 + } 484 self.write("</p>\n")?; 485 } 486 TagEnd::Heading(level) => {
+42 -42
crates/weaver-renderer/src/css.rs
··· 132 /* When sidenotes exist, body padding creates the gutter */ 133 /* Left padding shrinks first as viewport narrows, right stays for sidenotes */ 134 body:has(.sidenote) {{ 135 - padding-left: clamp(0rem, calc((100vw - 95ch - 15.5rem - 2rem) / 2), 15.5rem); 136 - padding-right: 15.5rem; 137 }} 138 139 /* Typography */ ··· 202 203 /* Lists */ 204 ul, ol {{ 205 - margin-left: 1rem; 206 margin-bottom: 1rem; 207 }} 208 ··· 250 251 /* Blockquotes */ 252 blockquote {{ 253 - border-left: 2px solid var(--color-secondary); 254 background: var(--color-surface); 255 - padding-left: 1rem; 256 - padding-right: 1rem; 257 padding-top: 0.5rem; 258 padding-bottom: 0.04rem; 259 margin: 1rem 0; ··· 276 th, td {{ 277 border: 1px solid var(--color-border); 278 padding: 0.5rem; 279 - text-align: left; 280 }} 281 282 th {{ ··· 318 319 .footnote-definition-label {{ 320 font-weight: 600; 321 - margin-right: 0.5rem; 322 color: var(--color-primary); 323 }} 324 325 /* Aside blocks (via WeaverBlock prefix) - scoped to notebook content */ 326 .notebook-content aside, 327 .notebook-content .aside {{ 328 - float: left; 329 width: 40%; 330 margin: 0 1.5rem 1rem 0; 331 padding: 1rem; 332 background: var(--color-surface); 333 - border-right: 3px solid var(--color-primary); 334 font-size: 0.9em; 335 - clear: left; 336 }} 337 338 .notebook-content aside > *:first-child, ··· 348 /* Reset blockquote styling inside asides */ 349 .notebook-content aside > blockquote, 350 .notebook-content .aside > blockquote {{ 351 - border-left: none; 352 background: transparent; 353 padding: 0; 354 margin: 0; ··· 356 }} 357 358 /* Indent utilities */ 359 - .indent-1 {{ margin-left: 1em; }} 360 - .indent-2 {{ margin-left: 2em; }} 361 - .indent-3 {{ margin-left: 3em; }} 362 363 /* Tufte-style Sidenotes */ 364 /* Hide checkbox for sidenote toggle */ ··· 377 position: relative; 378 top: -0.5em; 379 color: var(--color-primary); 380 - padding-left: 0.1em; 381 }} 382 383 /* Sidenote content (margin notes on wide screens) */ 384 .sidenote {{ 385 - float: right; 386 - clear: right; 387 - margin-right: -15.5rem; 388 width: 14rem; 389 margin-top: 0.3rem; 390 margin-bottom: 1rem; ··· 402 @media (max-width: 900px) {{ 403 /* Reset sidenote gutter on mobile */ 404 body:has(.sidenote) {{ 405 - padding-right: 0; 406 }} 407 408 aside, .aside {{ ··· 422 margin: 0.5rem 2.5%; 423 padding: 0.5rem; 424 background: var(--color-surface); 425 - border-left: 2px solid var(--color-primary); 426 }} 427 428 label.sidenote-number {{ ··· 460 margin: 1rem 0; 461 padding: 1rem; 462 background: var(--color-surface); 463 - border-left: 2px solid var(--color-secondary); 464 box-shadow: 0 1px 2px color-mix(in srgb, var(--color-text) 8%, transparent); 465 }} 466 467 .atproto-embed:hover {{ 468 - border-left-color: var(--color-primary); 469 }} 470 471 @media (prefers-color-scheme: dark) {{ 472 .atproto-embed {{ 473 box-shadow: none; 474 border: 1px solid var(--color-border); 475 - border-left: 2px solid var(--color-secondary); 476 }} 477 }} 478 ··· 649 }} 650 651 .embed-external:hover {{ 652 - border-left: 2px solid var(--color-primary); 653 - margin-left: -1px; 654 }} 655 656 @media (prefers-color-scheme: dark) {{ ··· 659 }} 660 661 .embed-external:hover {{ 662 - border-left: 2px solid var(--color-primary); 663 - margin-left: -1px; 664 }} 665 }} 666 ··· 746 margin-top: 0.5rem; 747 padding: 0.75rem; 748 background: var(--color-overlay); 749 - border-left: 2px solid var(--color-tertiary); 750 }} 751 752 @media (prefers-color-scheme: dark) {{ 753 .embed-quote {{ 754 border: 1px solid var(--color-border); 755 - border-left: 2px solid var(--color-tertiary); 756 }} 757 }} 758 ··· 783 display: block; 784 padding: 1rem; 785 background: var(--color-overlay); 786 - border-left: 2px solid var(--color-border); 787 color: var(--color-muted); 788 font-style: italic; 789 margin-top: 0.5rem; ··· 807 margin-top: 0.5rem; 808 padding: 0.75rem; 809 background: var(--color-overlay); 810 - border-left: 2px solid var(--color-tertiary); 811 }} 812 813 .embed-record-card > .embed-author-name {{ ··· 850 .embed-fields .embed-fields {{ 851 display: block; 852 margin-top: 0.5rem; 853 - margin-left: 1rem; 854 - padding-left: 0.5rem; 855 - border-left: 1px solid var(--color-border); 856 }} 857 858 /* Type label inside fields should be block with spacing */ ··· 967 padding: 0; 968 background: var(--color-surface); 969 border: 1px solid var(--color-border); 970 - border-left: 1px solid var(--color-border); 971 box-shadow: none; 972 overflow: hidden; 973 }} 974 975 .atproto-entry:hover {{ 976 - border-left-color: var(--color-border); 977 }} 978 979 @media (prefers-color-scheme: dark) {{ 980 .atproto-entry {{ 981 border: 1px solid var(--color-border); 982 - border-left: 1px solid var(--color-border); 983 }} 984 }} 985 ··· 1075 h3 {{ font-size: 1.2rem; }} 1076 1077 blockquote {{ 1078 - margin-left: 0; 1079 - margin-right: 0; 1080 }} 1081 }} 1082 ··· 1091 h3 {{ font-size: 1.1rem; }} 1092 1093 blockquote {{ 1094 - padding-left: 0.75rem; 1095 - padding-right: 0.75rem; 1096 }} 1097 }} 1098 "#,
··· 132 /* When sidenotes exist, body padding creates the gutter */ 133 /* Left padding shrinks first as viewport narrows, right stays for sidenotes */ 134 body:has(.sidenote) {{ 135 + padding-inline-start: clamp(0rem, calc((100vw - 95ch - 15.5rem - 2rem) / 2), 15.5rem); 136 + padding-inline-end: 15.5rem; 137 }} 138 139 /* Typography */ ··· 202 203 /* Lists */ 204 ul, ol {{ 205 + margin-inline-start: 1rem; 206 margin-bottom: 1rem; 207 }} 208 ··· 250 251 /* Blockquotes */ 252 blockquote {{ 253 + border-inline-start: 2px solid var(--color-secondary); 254 background: var(--color-surface); 255 + padding-inline-start: 1rem; 256 + padding-inline-end: 1rem; 257 padding-top: 0.5rem; 258 padding-bottom: 0.04rem; 259 margin: 1rem 0; ··· 276 th, td {{ 277 border: 1px solid var(--color-border); 278 padding: 0.5rem; 279 + text-align: start; 280 }} 281 282 th {{ ··· 318 319 .footnote-definition-label {{ 320 font-weight: 600; 321 + margin-inline-end: 0.5rem; 322 color: var(--color-primary); 323 }} 324 325 /* Aside blocks (via WeaverBlock prefix) - scoped to notebook content */ 326 .notebook-content aside, 327 .notebook-content .aside {{ 328 + float: inline-start; 329 width: 40%; 330 margin: 0 1.5rem 1rem 0; 331 padding: 1rem; 332 background: var(--color-surface); 333 + border-inline-end: 3px solid var(--color-primary); 334 font-size: 0.9em; 335 + clear: inline-start; 336 }} 337 338 .notebook-content aside > *:first-child, ··· 348 /* Reset blockquote styling inside asides */ 349 .notebook-content aside > blockquote, 350 .notebook-content .aside > blockquote {{ 351 + border-inline-start: none; 352 background: transparent; 353 padding: 0; 354 margin: 0; ··· 356 }} 357 358 /* Indent utilities */ 359 + .indent-1 {{ margin-inline-start: 1em; }} 360 + .indent-2 {{ margin-inline-start: 2em; }} 361 + .indent-3 {{ margin-inline-start: 3em; }} 362 363 /* Tufte-style Sidenotes */ 364 /* Hide checkbox for sidenote toggle */ ··· 377 position: relative; 378 top: -0.5em; 379 color: var(--color-primary); 380 + padding-inline-start: 0.1em; 381 }} 382 383 /* Sidenote content (margin notes on wide screens) */ 384 .sidenote {{ 385 + float: inline-end; 386 + clear: inline-end; 387 + margin-inline-end: -15.5rem; 388 width: 14rem; 389 margin-top: 0.3rem; 390 margin-bottom: 1rem; ··· 402 @media (max-width: 900px) {{ 403 /* Reset sidenote gutter on mobile */ 404 body:has(.sidenote) {{ 405 + padding-inline-end: 0; 406 }} 407 408 aside, .aside {{ ··· 422 margin: 0.5rem 2.5%; 423 padding: 0.5rem; 424 background: var(--color-surface); 425 + border-inline-start: 2px solid var(--color-primary); 426 }} 427 428 label.sidenote-number {{ ··· 460 margin: 1rem 0; 461 padding: 1rem; 462 background: var(--color-surface); 463 + border-inline-start: 2px solid var(--color-secondary); 464 box-shadow: 0 1px 2px color-mix(in srgb, var(--color-text) 8%, transparent); 465 }} 466 467 .atproto-embed:hover {{ 468 + border-inline-start-color: var(--color-primary); 469 }} 470 471 @media (prefers-color-scheme: dark) {{ 472 .atproto-embed {{ 473 box-shadow: none; 474 border: 1px solid var(--color-border); 475 + border-inline-start: 2px solid var(--color-secondary); 476 }} 477 }} 478 ··· 649 }} 650 651 .embed-external:hover {{ 652 + border-inline-start: 2px solid var(--color-primary); 653 + margin-inline-start: -1px; 654 }} 655 656 @media (prefers-color-scheme: dark) {{ ··· 659 }} 660 661 .embed-external:hover {{ 662 + border-inline-start: 2px solid var(--color-primary); 663 + margin-inline-start: -1px; 664 }} 665 }} 666 ··· 746 margin-top: 0.5rem; 747 padding: 0.75rem; 748 background: var(--color-overlay); 749 + border-inline-start: 2px solid var(--color-tertiary); 750 }} 751 752 @media (prefers-color-scheme: dark) {{ 753 .embed-quote {{ 754 border: 1px solid var(--color-border); 755 + border-inline-start: 2px solid var(--color-tertiary); 756 }} 757 }} 758 ··· 783 display: block; 784 padding: 1rem; 785 background: var(--color-overlay); 786 + border-inline-start: 2px solid var(--color-border); 787 color: var(--color-muted); 788 font-style: italic; 789 margin-top: 0.5rem; ··· 807 margin-top: 0.5rem; 808 padding: 0.75rem; 809 background: var(--color-overlay); 810 + border-inline-start: 2px solid var(--color-tertiary); 811 }} 812 813 .embed-record-card > .embed-author-name {{ ··· 850 .embed-fields .embed-fields {{ 851 display: block; 852 margin-top: 0.5rem; 853 + margin-inline-start: 1rem; 854 + padding-inline-start: 0.5rem; 855 + border-inline-start: 1px solid var(--color-border); 856 }} 857 858 /* Type label inside fields should be block with spacing */ ··· 967 padding: 0; 968 background: var(--color-surface); 969 border: 1px solid var(--color-border); 970 + border-inline-start: 1px solid var(--color-border); 971 box-shadow: none; 972 overflow: hidden; 973 }} 974 975 .atproto-entry:hover {{ 976 + border-inline-start-color: var(--color-border); 977 }} 978 979 @media (prefers-color-scheme: dark) {{ 980 .atproto-entry {{ 981 border: 1px solid var(--color-border); 982 + border-inline-start: 1px solid var(--color-border); 983 }} 984 }} 985 ··· 1075 h3 {{ font-size: 1.2rem; }} 1076 1077 blockquote {{ 1078 + margin-inline-start: 0; 1079 + margin-inline-end: 0; 1080 }} 1081 }} 1082 ··· 1091 h3 {{ font-size: 1.1rem; }} 1092 1093 blockquote {{ 1094 + padding-inline-start: 0.75rem; 1095 + padding-inline-end: 0.75rem; 1096 }} 1097 }} 1098 "#,
+2 -2
crates/weaver-renderer/src/static_site/snapshots/weaver_renderer__static_site__tests__blockquote_rendering.snap
··· 3 expression: output 4 --- 5 <blockquote> 6 - <p>This is a quote</p> 7 - <p>With multiple lines</p> 8 </blockquote>
··· 3 expression: output 4 --- 5 <blockquote> 6 + <p dir="ltr">This is a quote</p> 7 + <p dir="ltr">With multiple lines</p> 8 </blockquote>
+2 -2
crates/weaver-renderer/src/static_site/snapshots/weaver_renderer__static_site__tests__footnote_in_blockquote.snap
··· 3 expression: output 4 --- 5 <blockquote> 6 - <p>Quote with footnote<sup class="footnote-reference"><a href="#q">1</a></sup>.</p> 7 </blockquote> 8 <div class="footnote-definition" id="q"><sup class="footnote-definition-label">1</sup> 9 - <p>Footnote for quote.</p> 10 </div>
··· 3 expression: output 4 --- 5 <blockquote> 6 + <p dir="ltr">Quote with footnote<sup class="footnote-reference"><a href="#q">1</a></sup>.</p> 7 </blockquote> 8 <div class="footnote-definition" id="q"><sup class="footnote-definition-label">1</sup> 9 + <p dir="ltr">Footnote for quote.</p> 10 </div>
+3 -3
crates/weaver-renderer/src/static_site/snapshots/weaver_renderer__static_site__tests__footnote_multiple.snap
··· 2 source: crates/weaver-renderer/src/static_site/tests.rs 3 expression: output 4 --- 5 - <p>First<sup class="footnote-reference"><a href="#1">1</a></sup> and second<sup class="footnote-reference"><a href="#2">2</a></sup> footnotes.</p> 6 <div class="footnote-definition" id="1"><sup class="footnote-definition-label">1</sup> 7 - <p>First note.</p> 8 </div> 9 <div class="footnote-definition" id="2"><sup class="footnote-definition-label">2</sup> 10 - <p>Second note.</p> 11 </div>
··· 2 source: crates/weaver-renderer/src/static_site/tests.rs 3 expression: output 4 --- 5 + <p dir="ltr">First<sup class="footnote-reference"><a href="#1">1</a></sup> and second<sup class="footnote-reference"><a href="#2">2</a></sup> footnotes.</p> 6 <div class="footnote-definition" id="1"><sup class="footnote-definition-label">1</sup> 7 + <p dir="ltr">First note.</p> 8 </div> 9 <div class="footnote-definition" id="2"><sup class="footnote-definition-label">2</sup> 10 + <p dir="ltr">Second note.</p> 11 </div>
+1 -1
crates/weaver-renderer/src/static_site/snapshots/weaver_renderer__static_site__tests__footnote_named.snap
··· 2 source: crates/weaver-renderer/src/static_site/tests.rs 3 expression: output 4 --- 5 - <p>Reference<label for="sn-1" class="margin-toggle sidenote-number"></label><input type="checkbox" id="sn-1" class="margin-toggle"/><span class="sidenote">Named footnote content.</span>.</p>
··· 2 source: crates/weaver-renderer/src/static_site/tests.rs 3 expression: output 4 --- 5 + <p dir="ltr">Reference<label for="sn-1" class="sidenote-number"></label><input type="checkbox" id="sn-1" class="margin-toggle"/><span class="sidenote">Named footnote content.</span>.</p>
+1 -1
crates/weaver-renderer/src/static_site/snapshots/weaver_renderer__static_site__tests__footnote_sidenote_inline.snap
··· 2 source: crates/weaver-renderer/src/static_site/tests.rs 3 expression: output 4 --- 5 - <p>Here is text<label for="sn-1" class="margin-toggle sidenote-number"></label><input type="checkbox" id="sn-1" class="margin-toggle"/><span class="sidenote">Sidenote content.</span></p>
··· 2 source: crates/weaver-renderer/src/static_site/tests.rs 3 expression: output 4 --- 5 + <p dir="ltr">Here is text<label for="sn-1" class="sidenote-number"></label><input type="checkbox" id="sn-1" class="margin-toggle"/><span class="sidenote">Sidenote content.</span></p>
+1 -1
crates/weaver-renderer/src/static_site/snapshots/weaver_renderer__static_site__tests__footnote_traditional.snap
··· 2 source: crates/weaver-renderer/src/static_site/tests.rs 3 expression: output 4 --- 5 - <p>Here is some text<label for="sn-1" class="margin-toggle sidenote-number"></label><input type="checkbox" id="sn-1" class="margin-toggle"/><span class="sidenote">This is the footnote definition.</span>.</p>
··· 2 source: crates/weaver-renderer/src/static_site/tests.rs 3 expression: output 4 --- 5 + <p dir="ltr">Here is some text<label for="sn-1" class="sidenote-number"></label><input type="checkbox" id="sn-1" class="margin-toggle"/><span class="sidenote">This is the footnote definition.</span>.</p>
+1 -1
crates/weaver-renderer/src/static_site/snapshots/weaver_renderer__static_site__tests__footnote_with_inline_formatting.snap
··· 2 source: crates/weaver-renderer/src/static_site/tests.rs 3 expression: output 4 --- 5 - <p>Text with footnote<label for="sn-1" class="margin-toggle sidenote-number"></label><input type="checkbox" id="sn-1" class="margin-toggle"/><span class="sidenote">Note with <strong>bold</strong> and <em>italic</em>.</span>.</p>
··· 2 source: crates/weaver-renderer/src/static_site/tests.rs 3 expression: output 4 --- 5 + <p dir="ltr">Text with footnote<label for="sn-1" class="sidenote-number"></label><input type="checkbox" id="sn-1" class="margin-toggle"/><span class="sidenote">Note with <strong>bold</strong> and <em>italic</em>.</span>.</p>
+3 -3
crates/weaver-renderer/src/static_site/snapshots/weaver_renderer__static_site__tests__math_rendering.snap
··· 2 source: crates/weaver-renderer/src/static_site/tests.rs 3 expression: output 4 --- 5 - <p>Inline <span class="math math-inline">x^2</span> and display:</p> 6 - <p><span class="math math-display"> 7 y = mx + b 8 - </span></p>
··· 2 source: crates/weaver-renderer/src/static_site/tests.rs 3 expression: output 4 --- 5 + <p dir="ltr">Inline <span class="math math-inline">x^2</span> and display:</p> 6 + <span class="math math-display"> 7 y = mx + b 8 + </span><p></p>
+2 -2
crates/weaver-renderer/src/static_site/snapshots/weaver_renderer__static_site__tests__paragraph_rendering.snap
··· 2 source: crates/weaver-renderer/src/static_site/tests.rs 3 expression: output 4 --- 5 - <p>This is a paragraph.</p> 6 - <p>This is another paragraph.</p>
··· 2 source: crates/weaver-renderer/src/static_site/tests.rs 3 expression: output 4 --- 5 + <p dir="ltr">This is a paragraph.</p> 6 + <p dir="ltr">This is another paragraph.</p>
+1 -1
crates/weaver-renderer/src/static_site/snapshots/weaver_renderer__static_site__tests__weaver_block_aside_class.snap
··· 3 expression: output 4 --- 5 <aside> 6 - <p>This paragraph should be in an aside.</p> 7 </aside>
··· 3 expression: output 4 --- 5 <aside> 6 + <p dir="ltr">This paragraph should be in an aside.</p> 7 </aside>
+1 -1
crates/weaver-renderer/src/static_site/snapshots/weaver_renderer__static_site__tests__weaver_block_before_blockquote.snap
··· 4 --- 5 <aside> 6 <blockquote> 7 - <p>This blockquote is in an aside.</p> 8 </blockquote> 9 </aside>
··· 4 --- 5 <aside> 6 <blockquote> 7 + <p dir="ltr">This blockquote is in an aside.</p> 8 </blockquote> 9 </aside>
+1 -1
crates/weaver-renderer/src/static_site/snapshots/weaver_renderer__static_site__tests__weaver_block_before_heading.snap
··· 4 --- 5 <aside> 6 <h2>Heading in aside</h2> 7 - <p>Paragraph also in aside.</p> 8 </aside>
··· 4 --- 5 <aside> 6 <h2>Heading in aside</h2> 7 + <p dir="ltr">Paragraph also in aside.</p> 8 </aside>
+1 -1
crates/weaver-renderer/src/static_site/snapshots/weaver_renderer__static_site__tests__weaver_block_custom_attributes.snap
··· 3 expression: output 4 --- 5 <div class="foo" width="300px" data-test="value"> 6 - <p>Paragraph with class and attributes.</p> 7 </div>
··· 3 expression: output 4 --- 5 <div class="foo" width="300px" data-test="value"> 6 + <p dir="ltr">Paragraph with class and attributes.</p> 7 </div>
+1 -1
crates/weaver-renderer/src/static_site/snapshots/weaver_renderer__static_site__tests__weaver_block_custom_class.snap
··· 3 expression: output 4 --- 5 <div class="highlight"> 6 - <p>This paragraph has a custom class.</p> 7 </div>
··· 3 expression: output 4 --- 5 <div class="highlight"> 6 + <p dir="ltr">This paragraph has a custom class.</p> 7 </div>
+1 -1
crates/weaver-renderer/src/static_site/snapshots/weaver_renderer__static_site__tests__weaver_block_multiple_classes.snap
··· 3 expression: output 4 --- 5 <aside class="highlight important"> 6 - <p>Multiple classes applied.</p> 7 </aside>
··· 3 expression: output 4 --- 5 <aside class="highlight important"> 6 + <p dir="ltr">Multiple classes applied.</p> 7 </aside>
+2 -2
crates/weaver-renderer/src/static_site/snapshots/weaver_renderer__static_site__tests__weaver_block_no_effect_on_following.snap
··· 3 expression: output 4 --- 5 <aside> 6 - <p>First paragraph in aside.</p> 7 </aside> 8 - <p>Second paragraph NOT in aside.</p>
··· 3 expression: output 4 --- 5 <aside> 6 + <p dir="ltr">First paragraph in aside.</p> 7 </aside> 8 + <p dir="ltr">Second paragraph NOT in aside.</p>
+1 -1
crates/weaver-renderer/src/static_site/snapshots/weaver_renderer__static_site__tests__weaver_block_with_footnote.snap
··· 3 expression: output 4 --- 5 <aside> 6 - <p>Aside with a footnote<label for="sn-1" class="margin-toggle sidenote-number"></label><input type="checkbox" id="sn-1" class="margin-toggle"/><span class="sidenote">Footnote in aside context.</span>.</p> 7 </aside>
··· 3 expression: output 4 --- 5 <aside> 6 + <p dir="ltr">Aside with a footnote<label for="sn-1" class="sidenote-number"></label><input type="checkbox" id="sn-1" class="margin-toggle"/><span class="sidenote">Footnote in aside context.</span>.</p> 7 </aside>
+32 -4
crates/weaver-renderer/src/static_site/writer.rs
··· 48 in_sidenote: bool, 49 /// Whether we're deferring paragraph close for sidenote handling 50 defer_paragraph_close: bool, 51 } 52 53 impl<'input, I: Iterator<Item = Event<'input>>, A: AgentSession, W: StrWrite> ··· 72 pending_footnote_content: String::new(), 73 in_sidenote: false, 74 defer_paragraph_close: false, 75 } 76 } 77 ··· 130 /// Close deferred paragraph if we're in that state. 131 /// Called when a non-paragraph block element starts. 132 fn close_deferred_paragraph(&mut self) -> Result<(), W::Error> { 133 if self.defer_paragraph_close { 134 // Flush pending footnote as traditional before closing 135 self.flush_pending_footnote()?; ··· 252 self.close_wrapper()?; 253 self.defer_paragraph_close = false; 254 } else { 255 self.write("</p>\n")?; 256 self.block_depth -= 1; 257 self.close_wrapper()?; ··· 465 // Buffer text while waiting to see if footnote def follows 466 self.pending_footnote_content.push_str(&text); 467 } else if !self.in_non_writing_block { 468 escape_html_body_text(&mut self.writer, &text)?; 469 self.end_newline = text.ends_with('\n'); 470 } ··· 601 self.flush_pending_footnote()?; 602 self.emit_wrapper_start()?; 603 self.block_depth += 1; 604 - if self.end_newline { 605 - self.write("<p>") 606 } else { 607 - self.write("\n<p>") 608 - } 609 } 610 } 611 Tag::Heading {
··· 48 in_sidenote: bool, 49 /// Whether we're deferring paragraph close for sidenote handling 50 defer_paragraph_close: bool, 51 + /// Buffered paragraph opening tag (without closing `>`) for dir attribute emission 52 + pending_paragraph_open: Option<String>, 53 } 54 55 impl<'input, I: Iterator<Item = Event<'input>>, A: AgentSession, W: StrWrite> ··· 74 pending_footnote_content: String::new(), 75 in_sidenote: false, 76 defer_paragraph_close: false, 77 + pending_paragraph_open: None, 78 } 79 } 80 ··· 133 /// Close deferred paragraph if we're in that state. 134 /// Called when a non-paragraph block element starts. 135 fn close_deferred_paragraph(&mut self) -> Result<(), W::Error> { 136 + // Also flush any pending paragraph open (shouldn't happen in normal flow, but be defensive) 137 + if let Some(opening) = self.pending_paragraph_open.take() { 138 + self.write(&opening)?; 139 + self.write(">")?; 140 + } 141 if self.defer_paragraph_close { 142 // Flush pending footnote as traditional before closing 143 self.flush_pending_footnote()?; ··· 260 self.close_wrapper()?; 261 self.defer_paragraph_close = false; 262 } else { 263 + // Flush any pending paragraph open (for empty paragraphs) 264 + if let Some(opening) = self.pending_paragraph_open.take() { 265 + self.write(&opening)?; 266 + self.write(">")?; 267 + } 268 self.write("</p>\n")?; 269 self.block_depth -= 1; 270 self.close_wrapper()?; ··· 478 // Buffer text while waiting to see if footnote def follows 479 self.pending_footnote_content.push_str(&text); 480 } else if !self.in_non_writing_block { 481 + // Flush pending paragraph with dir attribute if needed 482 + if let Some(opening) = self.pending_paragraph_open.take() { 483 + if let Some(dir) = crate::utils::detect_text_direction(&text) { 484 + self.write(&opening)?; 485 + self.write(" dir=\"")?; 486 + self.write(dir)?; 487 + self.write("\">")?; 488 + } else { 489 + self.write(&opening)?; 490 + self.write(">")?; 491 + } 492 + } 493 escape_html_body_text(&mut self.writer, &text)?; 494 self.end_newline = text.ends_with('\n'); 495 } ··· 626 self.flush_pending_footnote()?; 627 self.emit_wrapper_start()?; 628 self.block_depth += 1; 629 + // Buffer paragraph opening for dir attribute detection 630 + let opening = if self.end_newline { 631 + String::from("<p") 632 } else { 633 + String::from("\n<p") 634 + }; 635 + self.pending_paragraph_open = Some(opening); 636 + Ok(()) 637 } 638 } 639 Tag::Heading {
+50 -1
crates/weaver-renderer/src/utils.rs
··· 1 - use markdown_weaver::{CodeBlockKind, CowStr, Event, Tag}; 2 use miette::IntoDiagnostic; 3 use n0_future::TryFutureExt; 4 use std::{path::Path, sync::OnceLock}; ··· 11 use markdown_weaver::BrokenLink; 12 use std::path::PathBuf; 13 use std::sync::Arc; 14 use unicode_normalization::UnicodeNormalization; 15 16 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))] ··· 207 } 208 } 209 }
··· 1 + use markdown_weaver::CowStr; 2 use miette::IntoDiagnostic; 3 use n0_future::TryFutureExt; 4 use std::{path::Path, sync::OnceLock}; ··· 11 use markdown_weaver::BrokenLink; 12 use std::path::PathBuf; 13 use std::sync::Arc; 14 + use unicode_bidi::{get_base_direction, Direction}; 15 use unicode_normalization::UnicodeNormalization; 16 17 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))] ··· 208 } 209 } 210 } 211 + 212 + /// Detect text direction from first strong directional character. 213 + /// Returns Some("rtl") for Hebrew/Arabic/etc, Some("ltr") for Latin, None if no strong char found. 214 + pub fn detect_text_direction(text: &str) -> Option<&'static str> { 215 + match get_base_direction(text) { 216 + Direction::Ltr => Some("ltr"), 217 + Direction::Rtl => Some("rtl"), 218 + Direction::Mixed => None, // neutral/unknown - let browser decide 219 + } 220 + } 221 + 222 + #[cfg(test)] 223 + mod tests { 224 + use super::*; 225 + 226 + #[test] 227 + fn test_detect_text_direction_ltr() { 228 + assert_eq!(detect_text_direction("Hello World"), Some("ltr")); 229 + assert_eq!(detect_text_direction("Привет мир"), Some("ltr")); 230 + assert_eq!(detect_text_direction("你好世界"), Some("ltr")); 231 + assert_eq!(detect_text_direction("Γειά σου κόσμε"), Some("ltr")); 232 + } 233 + 234 + #[test] 235 + fn test_detect_text_direction_rtl() { 236 + // Hebrew 237 + assert_eq!(detect_text_direction("שלום עולם"), Some("rtl")); 238 + // Arabic 239 + assert_eq!(detect_text_direction("مرحبا بالعالم"), Some("rtl")); 240 + // Mixed with leading whitespace and punctuation 241 + assert_eq!(detect_text_direction(" 123... שלום"), Some("rtl")); 242 + assert_eq!(detect_text_direction(" 456!!! مرحبا"), Some("rtl")); 243 + } 244 + 245 + #[test] 246 + fn test_detect_text_direction_neutral_only() { 247 + assert_eq!(detect_text_direction(" "), None); 248 + assert_eq!(detect_text_direction("123456"), None); 249 + assert_eq!(detect_text_direction("!!!..."), None); 250 + assert_eq!(detect_text_direction(""), None); 251 + } 252 + 253 + #[test] 254 + fn test_detect_text_direction_leading_neutrals() { 255 + assert_eq!(detect_text_direction(" 123... Hello"), Some("ltr")); 256 + assert_eq!(detect_text_direction("!!!456 שלום"), Some("rtl")); 257 + } 258 + }
-1
weaver_notes/test-sidenotes.md
··· 22 23 {.aside} 24 > **On Platform Decay** 25 - > 26 > Through all of this I was never really satisfied with the options that were out there for long-form writing. Wordpress required too much setup. Tumblr's system for comments remains insane. Hosting my own seemed like too much money to burn on something nobody might read. 27 28 But at the same time, Substack's success proves that there is very much a desire for long-form writing, enough that people will pay for it, and that investors will back it. There are thoughts and forms of writing that you simply cannot fit into a post or even a thread of posts.
··· 22 23 {.aside} 24 > **On Platform Decay** 25 > Through all of this I was never really satisfied with the options that were out there for long-form writing. Wordpress required too much setup. Tumblr's system for comments remains insane. Hosting my own seemed like too much money to burn on something nobody might read. 26 27 But at the same time, Substack's success proves that there is very much a desire for long-form writing, enough that people will pay for it, and that investors will back it. There are thoughts and forms of writing that you simply cannot fit into a post or even a thread of posts.