editor bugfixes, RTL text support

Orual f51cda4a fb1645bc

+1164 -730
+1
Cargo.lock
··· 11846 11846 "tokio", 11847 11847 "tokio-util", 11848 11848 "tracing", 11849 + "unicode-bidi", 11849 11850 "unicode-normalization", 11850 11851 "url", 11851 11852 "weaver-api",
+3 -3
crates/weaver-app/assets/styling/cards-base.css
··· 95 95 font-size: 0.875rem; 96 96 font-weight: 600; 97 97 margin-top: 0; 98 - margin-left: 0; 99 - margin-right: 0; 98 + margin-inline-start: 0; 99 + margin-inline-end: 0; 100 100 } 101 101 102 102 .card-preview p { ··· 122 122 ========================================================================== */ 123 123 124 124 .card-hover-border:hover { 125 - border-left-color: var(--color-secondary); 125 + border-inline-start-color: var(--color-secondary); 126 126 } 127 127 128 128 .card-hover-title:hover .card-title {
+48 -16
crates/weaver-app/assets/styling/editor.css
··· 159 159 background: var(--color-base); 160 160 border: 1px solid var(--color-overlay); 161 161 color: var(--color-text); 162 + /* break-spaces ensures trailing whitespace takes up space and allows cursor placement */ 163 + white-space-collapse: break-spaces; 162 164 } 163 165 164 166 .editor-content:focus { ··· 201 203 202 204 .editor-debug { 203 205 padding: 8px; 204 - padding-right: 0; 206 + padding-inline-end: 0; 205 207 background: var(--color-base); 206 208 font-family: var(--font-mono); 207 209 font-size: 12px; ··· 296 298 297 299 /* Editor page header with report button */ 298 300 .editor-header { 299 - padding-left: 6rem; 301 + padding-inline-start: 6rem; 300 302 background: var(--color-base); 301 303 } 302 304 ··· 323 325 324 326 .report-dialog-overlay { 325 327 position: fixed; 328 + inset-inline: 0; 326 329 top: 0; 327 - left: 0; 328 - right: 0; 329 330 bottom: 0; 330 331 background: rgba(0, 0, 0, 0.6); 331 332 display: flex; ··· 440 441 display: flex; 441 442 align-items: center; 442 443 gap: 12px; 443 - margin-left: auto; 444 + margin-inline-start: auto; 444 445 flex-shrink: 0; 445 446 } 446 447 ··· 466 467 467 468 .publish-dialog-overlay { 468 469 position: fixed; 470 + inset-inline: 0; 469 471 top: 0; 470 - left: 0; 471 - right: 0; 472 472 bottom: 0; 473 473 background: rgba(0, 0, 0, 0.6); 474 474 display: flex; ··· 795 795 font-weight: 500; 796 796 font-family: var(--font-mono); 797 797 text-transform: uppercase; 798 - margin-left: -6px; 798 + margin-inline-start: -6px; 799 799 position: relative; 800 800 overflow: hidden; 801 801 } ··· 808 808 } 809 809 810 810 .collab-avatar:first-child { 811 - margin-left: 0; 811 + margin-inline-start: 0; 812 812 } 813 813 814 814 .collab-avatar.collab-overflow { ··· 832 832 /* Collaborators panel overlay */ 833 833 .collaborators-overlay { 834 834 position: fixed; 835 + inset-inline: 0; 835 836 top: 0; 836 - left: 0; 837 - right: 0; 838 837 bottom: 0; 839 838 background: rgba(0, 0, 0, 0.4); 840 839 display: flex; ··· 953 952 954 953 .invite-dialog-overlay { 955 954 position: fixed; 955 + inset-inline: 0; 956 956 top: 0; 957 - left: 0; 958 - right: 0; 959 957 bottom: 0; 960 958 background: rgba(0, 0, 0, 0.4); 961 959 display: flex; ··· 1123 1121 margin: 6px 0 0 0; 1124 1122 padding: 8px; 1125 1123 background: var(--color-overlay); 1126 - border-left: 2px solid var(--color-border); 1124 + border-inline-start: 2px solid var(--color-border); 1127 1125 } 1128 1126 1129 1127 .invite-actions, ··· 1174 1172 1175 1173 .remote-cursors-overlay { 1176 1174 position: absolute; 1175 + inset-inline: 0; 1177 1176 top: 0; 1178 - left: 0; 1179 - right: 0; 1180 1177 bottom: 0; 1181 1178 pointer-events: none; 1182 1179 z-index: 10; ··· 1226 1223 opacity: 0.5; 1227 1224 } 1228 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 53 display: block; 54 54 width: 100%; 55 55 padding: 0.5rem 0.75rem; 56 - text-align: left; 56 + text-align: start; 57 57 background: none; 58 58 border: none; 59 59 cursor: pointer;
+12 -12
crates/weaver-app/assets/styling/entry-card.css
··· 12 12 display: block; 13 13 background: var(--color-surface); 14 14 box-shadow: 0 1px 2px color-mix(in srgb, var(--color-text) 6%, transparent); 15 - border-left: 2px solid transparent; 15 + border-inline-start: 2px solid transparent; 16 16 padding: 1.25rem; 17 17 text-decoration: none; 18 18 color: var(--color-text); ··· 23 23 24 24 .entry-card-link:hover { 25 25 box-shadow: 0 2px 4px color-mix(in srgb, var(--color-text) 10%, transparent); 26 - border-left-color: var(--color-secondary); 26 + border-inline-start-color: var(--color-secondary); 27 27 } 28 28 29 29 .entry-card-link:hover .entry-card-title { ··· 83 83 } 84 84 85 85 .feed-entry-card:hover { 86 - border-left: 2px solid var(--color-secondary); 87 - margin-left: -1px; 86 + border-inline-start: 2px solid var(--color-secondary); 87 + margin-inline-start: -1px; 88 88 } 89 89 90 90 .feed-entry-card:hover .entry-card-title { ··· 103 103 } 104 104 105 105 .feed-entry-card .entry-card-byline .entry-card-date { 106 - margin-left: auto; 106 + margin-inline-start: auto; 107 107 } 108 108 109 109 /* Date in header (no author) aligns right */ 110 110 .feed-entry-card .entry-card-header .entry-card-date { 111 - margin-left: auto; 111 + margin-inline-start: auto; 112 112 } 113 113 114 114 .entry-card-stats { ··· 121 121 122 122 .entry-card-stats .word-count::after { 123 123 content: "·"; 124 - margin-left: 0.5rem; 124 + margin-inline-start: 0.5rem; 125 125 } 126 126 127 127 .entry-card-tags { ··· 178 178 font-size: 0.875rem; 179 179 font-weight: 600; 180 180 margin-top: 0.5rem; 181 - margin-left: 0; 182 - margin-right: 0; 181 + margin-inline-start: 0; 182 + margin-inline-end: 0; 183 183 } 184 184 185 185 .entry-card-preview p { ··· 201 201 .feed-entry-card { 202 202 box-shadow: none; 203 203 border-top: 1px solid var(--color-border); 204 - border-right: 1px solid var(--color-border); 204 + border-inline-end: 1px solid var(--color-border); 205 205 border-bottom: 1px solid var(--color-border); 206 206 207 - border-left: 1px solid var(--color-border); 208 - /* Keep border-left as accent */ 207 + border-inline-start: 1px solid var(--color-border); 208 + /* Keep border-inline-start as accent */ 209 209 } 210 210 }
+8 -8
crates/weaver-app/assets/styling/entry.css
··· 104 104 105 105 .entry-footer-nav .nav-button-prev { 106 106 align-items: flex-start; 107 - text-align: left; 107 + text-align: start; 108 108 } 109 109 110 110 .entry-footer-nav .nav-button-next { 111 111 align-items: flex-end; 112 - text-align: right; 113 - margin-left: auto; 112 + text-align: end; 113 + margin-inline-start: auto; 114 114 } 115 115 116 116 .entry-footer-nav .nav-arrow { ··· 176 176 177 177 .entry-reading-stats { 178 178 color: var(--color-subtle); 179 - margin-left: auto; 179 + margin-inline-start: auto; 180 180 margin-top: 0.25rem; 181 181 } 182 182 183 183 .entry-reading-stats .word-count::after { 184 184 content: "·"; 185 - margin-left: 0.5rem; 185 + margin-inline-start: 0.5rem; 186 186 } 187 187 188 188 .entry-date { 189 - margin-left: auto; 189 + margin-inline-start: auto; 190 190 font-weight: 400; 191 191 align-items: last baseline; 192 192 color: var(--color-subtle); ··· 255 255 } 256 256 257 257 .entry-content-main blockquote { 258 - border-left-color: var(--color-secondary); 258 + border-inline-start-color: var(--color-secondary); 259 259 background: var(--color-surface); 260 260 } 261 261 ··· 292 292 293 293 .entry-header .nav-button-next { 294 294 order: 1; 295 - margin-left: auto; 295 + margin-inline-start: auto; 296 296 } 297 297 } 298 298
+3 -3
crates/weaver-app/assets/styling/home.css
··· 25 25 .pinned-items .entry-card, 26 26 .pinned-items .feed-entry-card, 27 27 .pinned-items .notebook-card { 28 - border-left: 3px solid var(--color-primary) !important; 28 + border-inline-start: 3px solid var(--color-primary) !important; 29 29 } 30 30 31 31 .pinned-items .feed-entry-card:hover { 32 - border-left: 3px solid var(--color-secondary) !important; 33 - margin-left: 0; 32 + border-inline-start: 3px solid var(--color-secondary) !important; 33 + margin-inline-start: 0; 34 34 } 35 35 36 36 .feed-section {
+1 -1
crates/weaver-app/assets/styling/invites.css
··· 72 72 margin: 0.5rem 0 0 0; 73 73 padding: 0.5rem; 74 74 background: var(--color-background); 75 - border-left: 3px solid var(--color-primary); 75 + border-inline-start: 3px solid var(--color-primary); 76 76 font-style: italic; 77 77 } 78 78
+4 -4
crates/weaver-app/assets/styling/navbar.css
··· 2 2 display: flex; 3 3 flex-direction: row; 4 4 justify-content: space-between; 5 - padding-left: 1rem; 5 + padding-inline-start: 1rem; 6 6 padding-top: 1rem; 7 - padding-right: 1rem; 7 + padding-inline-end: 1rem; 8 8 } 9 9 10 10 .breadcrumbs { ··· 48 48 display: flex; 49 49 align-items: center; 50 50 gap: 1rem; 51 - margin-left: auto; 52 - margin-right: 1rem; 51 + margin-inline-start: auto; 52 + margin-inline-end: 1rem; 53 53 } 54 54 55 55 .nav-tool-link {
+12 -12
crates/weaver-app/assets/styling/notebook-card.css
··· 19 19 display: block; 20 20 text-decoration: none; 21 21 color: var(--color-text); 22 - border-left: 3px solid transparent; 23 - padding-left: 0.75rem; 24 - margin-left: -0.75rem; 22 + border-inline-start: 3px solid transparent; 23 + padding-inline-start: 0.75rem; 24 + margin-inline-start: -0.75rem; 25 25 transition: border-color 0.2s ease; 26 26 } 27 27 28 28 .notebook-card-header-link:hover { 29 - border-left-color: var(--color-primary); 29 + border-inline-start-color: var(--color-primary); 30 30 } 31 31 32 32 .notebook-card-header-link:hover .notebook-card-title { ··· 130 130 .notebook-card-date { 131 131 color: var(--color-muted); 132 132 font-size: 0.85rem; 133 - margin-left: 0.25rem; 133 + margin-inline-start: 0.25rem; 134 134 } 135 135 136 136 .notebook-card-authors { ··· 157 157 display: block; 158 158 text-decoration: none; 159 159 color: var(--color-text); 160 - border-left: 2px solid transparent; 160 + border-inline-start: 2px solid transparent; 161 161 border-top: 1px solid var(--color-border); 162 - padding-left: 0.625rem; /* 0.5 grid */ 162 + padding-inline-start: 0.625rem; /* 0.5 grid */ 163 163 padding-top: 1.25rem; 164 - margin-left: -0.625rem; 165 - transition: border-left-color 0.2s ease; 164 + margin-inline-start: -0.625rem; 165 + transition: border-inline-start-color 0.2s ease; 166 166 } 167 167 168 168 .notebook-entry-preview-link:first-child { ··· 171 171 } 172 172 173 173 .notebook-entry-preview-link:hover { 174 - border-left-color: var(--color-secondary); 174 + border-inline-start-color: var(--color-secondary); 175 175 } 176 176 177 177 .notebook-entry-preview-link:hover .entry-preview-title { ··· 255 255 font-size: 0.875rem; 256 256 font-weight: 600; 257 257 margin-top: 0; 258 - margin-left: 0; 259 - margin-right: 0; 258 + margin-inline-start: 0; 259 + margin-inline-end: 0; 260 260 } 261 261 262 262 .entry-preview-content code {
+2 -2
crates/weaver-app/assets/styling/notebook-cover.css
··· 8 8 @media (min-width: 1400px) { 9 9 .notebook-cover { 10 10 border-top: 1px solid var(--color-border); 11 - border-right: 1px solid var(--color-border); 11 + border-inline-end: 1px solid var(--color-border); 12 12 border-bottom: 1px solid var(--color-border); 13 13 padding: 1.25rem; 14 14 } ··· 105 105 106 106 .notebook-cover-actions .button { 107 107 width: 100%; 108 - text-align: left; 108 + text-align: start; 109 109 border-radius: 0; 110 110 }
+2 -2
crates/weaver-app/assets/styling/profile-actions.css
··· 7 7 8 8 .profile-actions-container { 9 9 padding-top: 2.25rem; 10 - padding-right: 5rem; 10 + padding-inline-end: 5rem; 11 11 } 12 12 13 13 .profile-actions-title { ··· 33 33 /* Match notebook-add-entry styling */ 34 34 .profile-actions-list .button { 35 35 width: 100%; 36 - text-align: left; 36 + text-align: start; 37 37 font-size: 0.85rem; 38 38 color: var(--color-primary); 39 39 background: transparent;
+4 -4
crates/weaver-app/assets/styling/profile.css
··· 153 153 } 154 154 155 155 .profile-extras { 156 - padding-left: 1rem; 157 - border-left: 1.5px dashed var(--color-border); 156 + padding-inline-start: 1rem; 157 + border-inline-start: 1.5px dashed var(--color-border); 158 158 } 159 159 160 160 .profile-block { ··· 175 175 } 176 176 177 177 .profile-name-section { 178 - margin-left: 1rem; 178 + margin-inline-start: 1rem; 179 179 margin-top: -0.5rem; 180 180 } 181 181 } ··· 191 191 @media (min-width: 1400px) { 192 192 .profile-display { 193 193 border-top: 1.5px solid var(--color-border); 194 - border-right: 1.5px solid var(--color-border); 194 + border-inline-end: 1.5px solid var(--color-border); 195 195 } 196 196 } 197 197
+44 -44
crates/weaver-app/assets/styling/record-view.css
··· 73 73 .metadata-label::after { 74 74 content: ":"; 75 75 font-size: 0.8rem; 76 - margin-left: 0.25rem; 76 + margin-inline-start: 0.25rem; 77 77 } 78 78 79 79 .metadata-value { ··· 140 140 } 141 141 142 142 .tab-button.edit-button { 143 - margin-left: auto; 143 + margin-inline-start: auto; 144 144 } 145 145 146 146 .action-buttons-group { 147 - margin-left: auto; 147 + margin-inline-start: auto; 148 148 display: flex; 149 149 gap: 0; 150 150 align-items: center; ··· 182 182 padding: 0.5rem 1rem; 183 183 background: transparent; 184 184 border: none; 185 - text-align: left; 185 + text-align: start; 186 186 cursor: pointer; 187 187 color: var(--color-text); 188 188 font-family: var(--font-mono); ··· 200 200 display: flex; 201 201 flex-direction: column; 202 202 align-items: flex-start; 203 - border-left: 1px dashed var(--color-subtle); 203 + border-inline-start: 1px dashed var(--color-subtle); 204 204 } 205 205 206 206 .record-field { 207 207 display: flex; 208 208 flex-direction: column; 209 209 padding: 0rem 0 0rem 1rem; 210 - padding-right: 1rem; 211 - margin-left: -1px; 212 - border-left: 2px solid var(--color-secondary); 210 + padding-inline-end: 1rem; 211 + margin-inline-start: -1px; 212 + border-inline-start: 2px solid var(--color-secondary); 213 213 border-bottom: 1px dashed var(--color-subtle); 214 214 z-index: 1; 215 215 } 216 216 217 217 .record-field.field-error { 218 - border-left-color: var(--color-error); 218 + border-inline-start-color: var(--color-error); 219 219 background-color: rgba(255, 107, 107, 0.05); 220 220 } 221 221 ··· 224 224 font-size: 0.85rem; 225 225 color: var(--color-error); 226 226 padding: 0.25rem 0; 227 - border-left: 2px solid var(--color-error); 227 + border-inline-start: 2px solid var(--color-error); 228 228 border-bottom: 1px dashed var(--color-error); 229 - padding-left: 0.5rem; 229 + padding-inline-start: 0.5rem; 230 230 word-wrap: break-word; 231 - margin-left: -1px; 231 + margin-inline-start: -1px; 232 232 word-break: break-word; 233 233 overflow-wrap: break-word; 234 234 } ··· 254 254 color: var(--color-subtle); 255 255 font-size: 0.9rem; 256 256 font-weight: 400; 257 - padding-left: 0.125rem; 257 + padding-inline-start: 0.125rem; 258 258 padding-top: 0.5rem; 259 259 } 260 260 ··· 285 285 286 286 .record-section { 287 287 position: relative; 288 - margin-left: 1.5rem; 289 - border-left: 1px dashed var(--color-border); 288 + margin-inline-start: 1.5rem; 289 + border-inline-start: 1px dashed var(--color-border); 290 290 } 291 291 292 292 .section-content .record-section { 293 293 position: relative; 294 - border-left: 1px dashed var(--color-border); 294 + border-inline-start: 1px dashed var(--color-border); 295 295 } 296 296 297 297 .array-item .record-section { 298 298 position: relative; 299 - border-left: 1px dashed var(--color-border); 299 + border-inline-start: 1px dashed var(--color-border); 300 300 } 301 301 302 302 .section-label { ··· 304 304 color: var(--color-primary); 305 305 font-size: 1rem; 306 306 font-weight: 600; 307 - padding-left: 1rem; 307 + padding-inline-start: 1rem; 308 308 padding-top: 0.5rem; 309 309 padding-bottom: 0.25rem; 310 - margin-left: -1px; 311 - border-left: 2px solid var(--color-primary); 310 + margin-inline-start: -1px; 311 + border-inline-start: 2px solid var(--color-primary); 312 312 border-bottom: 1px dashed var(--color-muted); 313 313 } 314 314 ··· 317 317 color: var(--color-tertiary); 318 318 font-size: 0.9rem; 319 319 font-weight: 600; 320 - padding-left: 1rem; 320 + padding-inline-start: 1rem; 321 321 padding-top: 0.5rem; 322 - margin-left: -1px; 323 - border-left: 2px solid var(--color-secondary); 322 + margin-inline-start: -1px; 323 + border-inline-start: 2px solid var(--color-secondary); 324 324 } 325 325 326 326 .section-content { 327 327 display: flex; 328 328 flex-direction: column; 329 329 align-items: flex-start; 330 - padding-right: 1rem; 330 + padding-inline-end: 1rem; 331 331 } 332 332 333 333 .section-content .record-field { 334 - border-left-color: var(--color-secondary); 334 + border-inline-start-color: var(--color-secondary); 335 335 opacity: 0.95; 336 336 align-self: stretch; 337 337 width: 100%; ··· 536 536 537 537 .validation-errors .error { 538 538 padding: 0.25rem 0; 539 - border-left: 2px solid var(--color-error); 540 - padding-left: 0.5rem; 539 + border-inline-start: 2px solid var(--color-error); 540 + padding-inline-start: 0.5rem; 541 541 margin: 0.25rem 0; 542 542 word-wrap: break-word; 543 543 word-break: break-word; ··· 574 574 width: 100%; 575 575 display: flex; 576 576 flex-direction: column; 577 - margin-left: 1.48rem; 577 + margin-inline-start: 1.48rem; 578 578 } 579 579 580 580 /* Pretty Editor Input Fields */ ··· 627 627 .record-field textarea::-webkit-resizer { 628 628 background: transparent; 629 629 border: 2px solid var(--color-border); 630 - border-left: none; 630 + border-inline-start: none; 631 631 border-bottom: none; 632 632 border-top: none; 633 633 } ··· 657 657 background: var(--color-surface); 658 658 border: 1px solid var(--color-border); 659 659 padding: 0.25rem 0.25rem; 660 - margin-right: 0.5rem; 660 + margin-inline-end: 0.5rem; 661 661 margin-bottom: 0.2rem; 662 662 cursor: pointer; 663 663 transition: ··· 694 694 align-items: center; 695 695 padding: 0.75rem 0 0rem 0rem; 696 696 margin-bottom: 1rem; 697 - margin-left: 1.5rem; 698 - border-left: 1px solid var(--color-border); 697 + margin-inline-start: 1.5rem; 698 + border-inline-start: 1px solid var(--color-border); 699 699 } 700 700 701 701 .add-field-widget input[type="text"] { ··· 705 705 background: var(--color-background-alt, rgba(0, 0, 0, 0.2)); 706 706 border: 1px solid var(--color-border); 707 707 padding: 0.3rem 0.5rem; 708 - margin-left: -1px; 708 + margin-inline-start: -1px; 709 709 outline: none; 710 710 } 711 711 ··· 719 719 text-transform: uppercase; 720 720 letter-spacing: 0.05em; 721 721 padding: 0.3rem 0.75rem; 722 - margin-left: -1px; 722 + margin-inline-start: -1px; 723 723 background: transparent; 724 724 border: 1px solid var(--color-border); 725 725 color: var(--color-primary); ··· 733 733 text-transform: uppercase; 734 734 letter-spacing: 0.05em; 735 735 padding: 0.36rem 0.75rem; 736 - margin-left: -1px; 736 + margin-inline-start: -1px; 737 737 background: transparent; 738 738 border: 1px solid var(--color-primary); 739 739 color: var(--color-primary); ··· 799 799 background: var(--color-surface); 800 800 border: 1px dashed var(--color-border); 801 801 padding: 0.25rem 0.5rem; 802 - margin-right: 0.5rem; 802 + margin-inline-end: 0.5rem; 803 803 margin-bottom: -0.2rem; 804 804 cursor: pointer; 805 805 transition: ··· 933 933 934 934 .accordion-content { 935 935 grid-template-rows: 1fr; 936 - padding-left: 46px; 936 + padding-inline-start: 46px; 937 937 } 938 938 939 939 .accordion-content .section-content { 940 - margin-left: -1px; 940 + margin-inline-start: -1px; 941 941 } 942 942 943 943 .accordion-content .record-field { 944 - margin-left: 0px; 944 + margin-inline-start: 0px; 945 945 } 946 946 947 947 .accordion-content .array-item { 948 - margin-left: 0px; 948 + margin-inline-start: 0px; 949 949 } 950 950 951 951 .accordion-content .array-item .section-content { ··· 953 953 } 954 954 955 955 .accordion-content .array-item .record-section { 956 - margin-left: 1.5rem; 956 + margin-inline-start: 1.5rem; 957 957 } 958 958 959 959 .accordion-content .array-item .record-section .record-field { 960 - margin-left: -1px; 960 + margin-inline-start: -1px; 961 961 } 962 962 963 963 .accordion-content .array-item .record-section .accordion-trigger .section-label { 964 - margin-left: -2px; 964 + margin-inline-start: -2px; 965 965 } 966 966 967 967 .accordion-trigger { ··· 969 969 } 970 970 971 971 .accordion { 972 - margin-left: -46px; 972 + margin-inline-start: -46px; 973 973 }
+42 -15
crates/weaver-app/src/components/editor/beforeinput.rs
··· 288 288 // === Insertion === 289 289 InputType::InsertText => { 290 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 291 + use super::FORCE_INNERHTML_UPDATE; 292 + 295 293 let action = EditorAction::Insert { 296 294 text: text.clone(), 297 295 range, 298 296 }; 299 297 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 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 + } 308 325 } else { 309 326 BeforeInputResult::PassThrough 310 327 } ··· 372 389 }; 373 390 374 391 if needs_special_handling { 375 - // Complex delete - we handle everything, prevent browser default 392 + // Handle fully when: complex delete OR when dom_sync will replace innerHTML 393 + // (FORCE_INNERHTML_UPDATE). PassThrough + innerHTML causes double-deletion. 376 394 let action = EditorAction::DeleteBackward { range }; 377 395 execute_action(doc, &action); 378 396 BeforeInputResult::Handled ··· 388 406 doc.selection.set(None); 389 407 } 390 408 tracing::debug!("deleteContentBackward: after model update, returning PassThrough"); 391 - BeforeInputResult::PassThrough 409 + if super::FORCE_INNERHTML_UPDATE { 410 + BeforeInputResult::Handled 411 + } else { 412 + BeforeInputResult::PassThrough 413 + } 392 414 } 393 415 } 394 416 ··· 404 426 }; 405 427 406 428 if needs_special_handling { 429 + // Handle fully when: complex delete OR when dom_sync will replace innerHTML 407 430 let action = EditorAction::DeleteForward { range }; 408 431 execute_action(doc, &action); 409 432 BeforeInputResult::Handled ··· 413 436 let _ = doc.remove_tracked(range.start, 1); 414 437 doc.selection.set(None); 415 438 } 416 - BeforeInputResult::PassThrough 439 + if super::FORCE_INNERHTML_UPDATE { 440 + BeforeInputResult::Handled 441 + } else { 442 + BeforeInputResult::PassThrough 443 + } 417 444 } 418 445 } 419 446
+12 -3
crates/weaver-app/src/components/editor/dom_sync.rs
··· 496 496 } 497 497 498 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; 499 + use super::FORCE_INNERHTML_UPDATE; 502 500 503 501 // For cursor paragraph: only update if syntax/formatting changed 504 502 // This prevents destroying browser selection during fast typing ··· 549 547 // Update hash - browser native editing has the correct content 550 548 let _ = existing_elem.set_attribute("data-hash", &new_hash); 551 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 + 552 561 // Timing instrumentation for innerHTML update cost 553 562 let start = web_sys::window() 554 563 .and_then(|w| w.performance())
+9
crates/weaver-app/src/components/editor/mod.rs
··· 32 32 #[cfg(test)] 33 33 mod tests; 34 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 + 35 44 // Main component 36 45 pub use component::MarkdownEditor; 37 46
+2
crates/weaver-app/src/components/editor/publish.rs
··· 250 250 .title(doc.title()) 251 251 .path(path) 252 252 .created_at(Datetime::now()) 253 + .updated_at(Datetime::now()) 253 254 .maybe_tags(tags) 254 255 .maybe_embeds(entry_embeds) 255 256 .build(); ··· 400 401 .title(doc.title()) 401 402 .path(path) 402 403 .created_at(Datetime::now()) 404 + .updated_at(Datetime::now()) 403 405 .maybe_tags(tags) 404 406 .maybe_embeds(entry_embeds) 405 407 .build();
+11
crates/weaver-app/src/components/editor/render.rs
··· 184 184 let fn_start = crate::perf::now(); 185 185 let source = text.to_string(); 186 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 + 187 198 // Handle empty document 188 199 if source.is_empty() { 189 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 8 char_range: 9 9 - 0 10 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" 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 12 offset_map: 13 13 - byte_range: 14 14 - 2 ··· 16 16 char_range: 17 17 - 0 18 18 - 0 19 - node_id: n0 19 + node_id: p-0-n0 20 20 char_offset_in_node: 0 21 21 child_index: 0 22 22 utf16_len: 0 ··· 26 26 char_range: 27 27 - 0 28 28 - 1 29 - node_id: n0 29 + node_id: p-0-n0 30 30 char_offset_in_node: 0 31 31 child_index: ~ 32 32 utf16_len: 1 ··· 36 36 char_range: 37 37 - 1 38 38 - 2 39 - node_id: n0 39 + node_id: p-0-n0 40 40 char_offset_in_node: 1 41 41 child_index: ~ 42 42 utf16_len: 1 ··· 46 46 char_range: 47 47 - 2 48 48 - 17 49 - node_id: n0 49 + node_id: p-0-n0 50 50 char_offset_in_node: 2 51 51 child_index: ~ 52 52 utf16_len: 15 ··· 56 56 char_range: 57 57 - 17 58 58 - 18 59 - node_id: n0 59 + node_id: p-0-n0 60 60 char_offset_in_node: 17 61 61 child_index: ~ 62 62 utf16_len: 1 ··· 86 86 char_range: 87 87 - 22 88 88 - 41 89 - html: "<p id=\"n1\">With multiple lines</p>\n" 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 90 offset_map: 91 91 - byte_range: 92 - - 0 93 - - 0 92 + - 18 93 + - 22 94 94 char_range: 95 + - 18 95 96 - 22 97 + node_id: p-1-n0 98 + char_offset_in_node: 0 99 + child_index: ~ 100 + utf16_len: 4 101 + - byte_range: 96 102 - 22 97 - node_id: n1 103 + - 22 104 + char_range: 105 + - 22 106 + - 22 107 + node_id: p-1-n1 98 108 char_offset_in_node: 0 99 109 child_index: 0 100 110 utf16_len: 0 101 111 - byte_range: 102 - - 0 103 - - 19 112 + - 22 113 + - 41 104 114 char_range: 105 115 - 22 106 116 - 41 107 - node_id: n1 117 + node_id: p-1-n1 108 118 char_offset_in_node: 0 109 119 child_index: ~ 110 120 utf16_len: 19
+7 -7
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__bold.snap
··· 8 8 char_range: 9 9 - 0 10 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" 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 12 offset_map: 13 13 - byte_range: 14 14 - 0 ··· 16 16 char_range: 17 17 - 0 18 18 - 0 19 - node_id: n0 19 + node_id: p-0-n0 20 20 char_offset_in_node: 0 21 21 child_index: 0 22 22 utf16_len: 0 ··· 26 26 char_range: 27 27 - 0 28 28 - 5 29 - node_id: n0 29 + node_id: p-0-n0 30 30 char_offset_in_node: 0 31 31 child_index: ~ 32 32 utf16_len: 5 ··· 36 36 char_range: 37 37 - 5 38 38 - 7 39 - node_id: n0 39 + node_id: p-0-n0 40 40 char_offset_in_node: 5 41 41 child_index: ~ 42 42 utf16_len: 2 ··· 46 46 char_range: 47 47 - 7 48 48 - 11 49 - node_id: n0 49 + node_id: p-0-n0 50 50 char_offset_in_node: 7 51 51 child_index: ~ 52 52 utf16_len: 4 ··· 56 56 char_range: 57 57 - 11 58 58 - 13 59 - node_id: n0 59 + node_id: p-0-n0 60 60 char_offset_in_node: 11 61 61 child_index: ~ 62 62 utf16_len: 2 ··· 66 66 char_range: 67 67 - 13 68 68 - 18 69 - node_id: n0 69 + node_id: p-0-n0 70 70 char_offset_in_node: 13 71 71 child_index: ~ 72 72 utf16_len: 5
+9 -9
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__bold_italic.snap
··· 8 8 char_range: 9 9 - 0 10 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" 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 12 offset_map: 13 13 - byte_range: 14 14 - 0 ··· 16 16 char_range: 17 17 - 0 18 18 - 0 19 - node_id: n0 19 + node_id: p-0-n0 20 20 char_offset_in_node: 0 21 21 child_index: 0 22 22 utf16_len: 0 ··· 26 26 char_range: 27 27 - 0 28 28 - 5 29 - node_id: n0 29 + node_id: p-0-n0 30 30 char_offset_in_node: 0 31 31 child_index: ~ 32 32 utf16_len: 5 ··· 36 36 char_range: 37 37 - 5 38 38 - 6 39 - node_id: n0 39 + node_id: p-0-n0 40 40 char_offset_in_node: 5 41 41 child_index: ~ 42 42 utf16_len: 1 ··· 46 46 char_range: 47 47 - 6 48 48 - 8 49 - node_id: n0 49 + node_id: p-0-n0 50 50 char_offset_in_node: 6 51 51 child_index: ~ 52 52 utf16_len: 2 ··· 56 56 char_range: 57 57 - 8 58 58 - 19 59 - node_id: n0 59 + node_id: p-0-n0 60 60 char_offset_in_node: 8 61 61 child_index: ~ 62 62 utf16_len: 11 ··· 66 66 char_range: 67 67 - 19 68 68 - 21 69 - node_id: n0 69 + node_id: p-0-n0 70 70 char_offset_in_node: 19 71 71 child_index: ~ 72 72 utf16_len: 2 ··· 76 76 char_range: 77 77 - 21 78 78 - 22 79 - node_id: n0 79 + node_id: p-0-n0 80 80 char_offset_in_node: 21 81 81 child_index: ~ 82 82 utf16_len: 1 ··· 86 86 char_range: 87 87 - 22 88 88 - 27 89 - node_id: n0 89 + node_id: p-0-n0 90 90 char_offset_in_node: 22 91 91 child_index: ~ 92 92 utf16_len: 5
+2 -2
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__code_block_fenced.snap
··· 8 8 char_range: 9 9 - 0 10 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>" 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 12 offset_map: 13 13 - byte_range: 14 14 - 8 ··· 16 16 char_range: 17 17 - 8 18 18 - 21 19 - node_id: n0 19 + node_id: p-0-n0 20 20 char_offset_in_node: 0 21 21 child_index: ~ 22 22 utf16_len: 13
+22 -12
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__gap_between_blocks.snap
··· 8 8 char_range: 9 9 - 0 10 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" 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 12 offset_map: 13 13 - byte_range: 14 14 - 0 ··· 16 16 char_range: 17 17 - 0 18 18 - 0 19 - node_id: n0 19 + node_id: p-0-n0 20 20 char_offset_in_node: 0 21 21 child_index: 0 22 22 utf16_len: 0 ··· 26 26 char_range: 27 27 - 0 28 28 - 2 29 - node_id: n0 29 + node_id: p-0-n0 30 30 char_offset_in_node: 0 31 31 child_index: ~ 32 32 utf16_len: 2 ··· 36 36 char_range: 37 37 - 2 38 38 - 9 39 - node_id: n0 39 + node_id: p-0-n0 40 40 char_offset_in_node: 2 41 41 child_index: ~ 42 42 utf16_len: 7 ··· 46 46 char_range: 47 47 - 9 48 48 - 10 49 - node_id: n0 49 + node_id: p-0-n0 50 50 char_offset_in_node: 9 51 51 child_index: ~ 52 52 utf16_len: 1 ··· 57 57 char_range: 58 58 - 11 59 59 - 26 60 - html: "<p id=\"n1\">Paragraph below</p>\n" 60 + html: "<span id=\"p-1-n0\">\n</span>\n<p id=\"p-1-n1\" dir=\"ltr\">Paragraph below</p>\n" 61 61 offset_map: 62 62 - byte_range: 63 - - 0 64 - - 0 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 65 75 char_range: 66 76 - 11 67 77 - 11 68 - node_id: n1 78 + node_id: p-1-n1 69 79 char_offset_in_node: 0 70 80 child_index: 0 71 81 utf16_len: 0 72 82 - byte_range: 73 - - 0 74 - - 15 83 + - 11 84 + - 26 75 85 char_range: 76 86 - 11 77 87 - 26 78 - node_id: n1 88 + node_id: p-1-n1 79 89 char_offset_in_node: 0 80 90 child_index: ~ 81 91 utf16_len: 15
+6 -6
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__hard_break.snap
··· 8 8 char_range: 9 9 - 0 10 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" 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 12 offset_map: 13 13 - byte_range: 14 14 - 0 ··· 16 16 char_range: 17 17 - 0 18 18 - 0 19 - node_id: n0 19 + node_id: p-0-n0 20 20 char_offset_in_node: 0 21 21 child_index: 0 22 22 utf16_len: 0 ··· 26 26 char_range: 27 27 - 0 28 28 - 8 29 - node_id: n0 29 + node_id: p-0-n0 30 30 char_offset_in_node: 0 31 31 child_index: ~ 32 32 utf16_len: 8 ··· 36 36 char_range: 37 37 - 8 38 38 - 10 39 - node_id: n0 39 + node_id: p-0-n0 40 40 char_offset_in_node: 8 41 41 child_index: ~ 42 42 utf16_len: 2 ··· 46 46 char_range: 47 47 - 10 48 48 - 11 49 - node_id: n0 49 + node_id: p-0-n0 50 50 char_offset_in_node: 10 51 51 child_index: ~ 52 52 utf16_len: 1 ··· 56 56 char_range: 57 57 - 11 58 58 - 19 59 - node_id: n0 59 + node_id: p-0-n0 60 60 char_offset_in_node: 11 61 61 child_index: ~ 62 62 utf16_len: 8
+4 -4
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__heading_h1.snap
··· 8 8 char_range: 9 9 - 0 10 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" 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 12 offset_map: 13 13 - byte_range: 14 14 - 0 ··· 16 16 char_range: 17 17 - 0 18 18 - 0 19 - node_id: n0 19 + node_id: p-0-n0 20 20 char_offset_in_node: 0 21 21 child_index: 0 22 22 utf16_len: 0 ··· 26 26 char_range: 27 27 - 0 28 28 - 2 29 - node_id: n0 29 + node_id: p-0-n0 30 30 char_offset_in_node: 0 31 31 child_index: ~ 32 32 utf16_len: 2 ··· 36 36 char_range: 37 37 - 2 38 38 - 11 39 - node_id: n0 39 + node_id: p-0-n0 40 40 char_offset_in_node: 2 41 41 child_index: ~ 42 42 utf16_len: 9
+71 -41
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__heading_levels.snap
··· 8 8 char_range: 9 9 - 0 10 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" 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 12 offset_map: 13 13 - byte_range: 14 14 - 0 ··· 16 16 char_range: 17 17 - 0 18 18 - 0 19 - node_id: n0 19 + node_id: p-0-n0 20 20 char_offset_in_node: 0 21 21 child_index: 0 22 22 utf16_len: 0 ··· 26 26 char_range: 27 27 - 0 28 28 - 2 29 - node_id: n0 29 + node_id: p-0-n0 30 30 char_offset_in_node: 0 31 31 child_index: ~ 32 32 utf16_len: 2 ··· 36 36 char_range: 37 37 - 2 38 38 - 4 39 - node_id: n0 39 + node_id: p-0-n0 40 40 char_offset_in_node: 2 41 41 child_index: ~ 42 42 utf16_len: 2 ··· 46 46 char_range: 47 47 - 4 48 48 - 5 49 - node_id: n0 49 + node_id: p-0-n0 50 50 char_offset_in_node: 4 51 51 child_index: ~ 52 52 utf16_len: 1 ··· 57 57 char_range: 58 58 - 6 59 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" 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 61 offset_map: 62 62 - byte_range: 63 - - 0 64 - - 0 63 + - 5 64 + - 6 65 65 char_range: 66 + - 5 66 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: 67 76 - 6 68 - node_id: n1 77 + - 6 78 + node_id: p-1-n1 69 79 char_offset_in_node: 0 70 80 child_index: 0 71 81 utf16_len: 0 72 82 - byte_range: 73 - - 0 74 - - 3 83 + - 6 84 + - 9 75 85 char_range: 76 86 - 6 77 87 - 9 78 - node_id: n1 88 + node_id: p-1-n1 79 89 char_offset_in_node: 0 80 90 child_index: ~ 81 91 utf16_len: 3 82 92 - byte_range: 83 - - 3 84 - - 5 93 + - 9 94 + - 11 85 95 char_range: 86 96 - 9 87 97 - 11 88 - node_id: n1 98 + node_id: p-1-n1 89 99 char_offset_in_node: 3 90 100 child_index: ~ 91 101 utf16_len: 2 92 102 - byte_range: 93 - - 5 94 - - 6 103 + - 11 104 + - 12 95 105 char_range: 96 106 - 11 97 107 - 12 98 - node_id: n1 108 + node_id: p-1-n1 99 109 char_offset_in_node: 5 100 110 child_index: ~ 101 111 utf16_len: 1 ··· 106 116 char_range: 107 117 - 13 108 118 - 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" 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" 110 120 offset_map: 111 121 - byte_range: 112 - - 0 113 - - 0 122 + - 12 123 + - 13 114 124 char_range: 125 + - 12 115 126 - 13 127 + node_id: p-2-n0 128 + char_offset_in_node: 0 129 + child_index: ~ 130 + utf16_len: 1 131 + - byte_range: 116 132 - 13 117 - node_id: n2 133 + - 13 134 + char_range: 135 + - 13 136 + - 13 137 + node_id: p-2-n1 118 138 char_offset_in_node: 0 119 139 child_index: 0 120 140 utf16_len: 0 121 141 - byte_range: 122 - - 0 123 - - 4 142 + - 13 143 + - 17 124 144 char_range: 125 145 - 13 126 146 - 17 127 - node_id: n2 147 + node_id: p-2-n1 128 148 char_offset_in_node: 0 129 149 child_index: ~ 130 150 utf16_len: 4 131 151 - byte_range: 132 - - 4 133 - - 6 152 + - 17 153 + - 19 134 154 char_range: 135 155 - 17 136 156 - 19 137 - node_id: n2 157 + node_id: p-2-n1 138 158 char_offset_in_node: 4 139 159 child_index: ~ 140 160 utf16_len: 2 141 161 - byte_range: 142 - - 6 143 - - 7 162 + - 19 163 + - 20 144 164 char_range: 145 165 - 19 146 166 - 20 147 - node_id: n2 167 + node_id: p-2-n1 148 168 char_offset_in_node: 6 149 169 child_index: ~ 150 170 utf16_len: 1 ··· 155 175 char_range: 156 176 - 21 157 177 - 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" 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" 159 179 offset_map: 160 180 - byte_range: 161 - - 0 162 - - 0 181 + - 20 182 + - 21 163 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: 164 191 - 21 165 192 - 21 166 - node_id: n3 193 + char_range: 194 + - 21 195 + - 21 196 + node_id: p-3-n1 167 197 char_offset_in_node: 0 168 198 child_index: 0 169 199 utf16_len: 0 170 200 - byte_range: 171 - - 0 172 - - 5 201 + - 21 202 + - 26 173 203 char_range: 174 204 - 21 175 205 - 26 176 - node_id: n3 206 + node_id: p-3-n1 177 207 char_offset_in_node: 0 178 208 child_index: ~ 179 209 utf16_len: 5 180 210 - byte_range: 181 - - 5 182 - - 7 211 + - 26 212 + - 28 183 213 char_range: 184 214 - 26 185 215 - 28 186 - node_id: n3 216 + node_id: p-3-n1 187 217 char_offset_in_node: 5 188 218 child_index: ~ 189 219 utf16_len: 2
+5 -5
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__inline_code.snap
··· 8 8 char_range: 9 9 - 0 10 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" 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 12 offset_map: 13 13 - byte_range: 14 14 - 0 ··· 16 16 char_range: 17 17 - 0 18 18 - 0 19 - node_id: n0 19 + node_id: p-0-n0 20 20 char_offset_in_node: 0 21 21 child_index: 0 22 22 utf16_len: 0 ··· 26 26 char_range: 27 27 - 0 28 28 - 5 29 - node_id: n0 29 + node_id: p-0-n0 30 30 char_offset_in_node: 0 31 31 child_index: ~ 32 32 utf16_len: 5 ··· 36 36 char_range: 37 37 - 6 38 38 - 10 39 - node_id: n0 39 + node_id: p-0-n0 40 40 char_offset_in_node: 5 41 41 child_index: ~ 42 42 utf16_len: 6 ··· 46 46 char_range: 47 47 - 11 48 48 - 16 49 - node_id: n0 49 + node_id: p-0-n0 50 50 char_offset_in_node: 11 51 51 child_index: ~ 52 52 utf16_len: 5
+7 -7
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__italic.snap
··· 8 8 char_range: 9 9 - 0 10 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" 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 12 offset_map: 13 13 - byte_range: 14 14 - 0 ··· 16 16 char_range: 17 17 - 0 18 18 - 0 19 - node_id: n0 19 + node_id: p-0-n0 20 20 char_offset_in_node: 0 21 21 child_index: 0 22 22 utf16_len: 0 ··· 26 26 char_range: 27 27 - 0 28 28 - 5 29 - node_id: n0 29 + node_id: p-0-n0 30 30 char_offset_in_node: 0 31 31 child_index: ~ 32 32 utf16_len: 5 ··· 36 36 char_range: 37 37 - 5 38 38 - 6 39 - node_id: n0 39 + node_id: p-0-n0 40 40 char_offset_in_node: 5 41 41 child_index: ~ 42 42 utf16_len: 1 ··· 46 46 char_range: 47 47 - 6 48 48 - 12 49 - node_id: n0 49 + node_id: p-0-n0 50 50 char_offset_in_node: 6 51 51 child_index: ~ 52 52 utf16_len: 6 ··· 56 56 char_range: 57 57 - 12 58 58 - 13 59 - node_id: n0 59 + node_id: p-0-n0 60 60 char_offset_in_node: 12 61 61 child_index: ~ 62 62 utf16_len: 1 ··· 66 66 char_range: 67 67 - 13 68 68 - 18 69 - node_id: n0 69 + node_id: p-0-n0 70 70 char_offset_in_node: 13 71 71 child_index: ~ 72 72 utf16_len: 5
+3 -3
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__mixed_unicode_ascii.snap
··· 8 8 char_range: 9 9 - 0 10 10 - 16 11 - html: "<p id=\"n0\">Hello 你好 world 🎉</p>\n" 11 + html: "<p id=\"p-0-n0\" dir=\"ltr\">Hello 你好 world 🎉</p>\n" 12 12 offset_map: 13 13 - byte_range: 14 14 - 0 ··· 16 16 char_range: 17 17 - 0 18 18 - 0 19 - node_id: n0 19 + node_id: p-0-n0 20 20 char_offset_in_node: 0 21 21 child_index: 0 22 22 utf16_len: 0 ··· 26 26 char_range: 27 27 - 0 28 28 - 16 29 - node_id: n0 29 + node_id: p-0-n0 30 30 char_offset_in_node: 0 31 31 child_index: ~ 32 32 utf16_len: 17
+21 -11
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__multiple_blank_lines.snap
··· 8 8 char_range: 9 9 - 0 10 10 - 6 11 - html: "<p id=\"n0\">First\n</p>\n" 11 + html: "<p id=\"p-0-n0\" dir=\"ltr\">First\n</p>\n" 12 12 offset_map: 13 13 - byte_range: 14 14 - 0 ··· 16 16 char_range: 17 17 - 0 18 18 - 0 19 - node_id: n0 19 + node_id: p-0-n0 20 20 char_offset_in_node: 0 21 21 child_index: 0 22 22 utf16_len: 0 ··· 26 26 char_range: 27 27 - 0 28 28 - 5 29 - node_id: n0 29 + node_id: p-0-n0 30 30 char_offset_in_node: 0 31 31 child_index: ~ 32 32 utf16_len: 5 ··· 36 36 char_range: 37 37 - 5 38 38 - 6 39 - node_id: n0 39 + node_id: p-0-n0 40 40 char_offset_in_node: 5 41 41 child_index: ~ 42 42 utf16_len: 1 ··· 66 66 char_range: 67 67 - 9 68 68 - 15 69 - html: "<p id=\"n1\">Second</p>\n" 69 + html: "<span id=\"p-1-n0\">\n\n\n</span>\n<p id=\"p-1-n1\" dir=\"ltr\">Second</p>\n" 70 70 offset_map: 71 71 - byte_range: 72 - - 0 73 - - 0 72 + - 6 73 + - 9 74 74 char_range: 75 + - 6 75 76 - 9 77 + node_id: p-1-n0 78 + char_offset_in_node: 0 79 + child_index: ~ 80 + utf16_len: 3 81 + - byte_range: 76 82 - 9 77 - node_id: n1 83 + - 9 84 + char_range: 85 + - 9 86 + - 9 87 + node_id: p-1-n1 78 88 char_offset_in_node: 0 79 89 child_index: 0 80 90 utf16_len: 0 81 91 - byte_range: 82 - - 0 83 - - 6 92 + - 9 93 + - 15 84 94 char_range: 85 95 - 9 86 96 - 15 87 - node_id: n1 97 + node_id: p-1-n1 88 98 char_offset_in_node: 0 89 99 child_index: ~ 90 100 utf16_len: 6
+11 -11
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__multiple_inline_formats.snap
··· 8 8 char_range: 9 9 - 0 10 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" 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 12 offset_map: 13 13 - byte_range: 14 14 - 0 ··· 16 16 char_range: 17 17 - 0 18 18 - 0 19 - node_id: n0 19 + node_id: p-0-n0 20 20 char_offset_in_node: 0 21 21 child_index: 0 22 22 utf16_len: 0 ··· 26 26 char_range: 27 27 - 0 28 28 - 2 29 - node_id: n0 29 + node_id: p-0-n0 30 30 char_offset_in_node: 0 31 31 child_index: ~ 32 32 utf16_len: 2 ··· 36 36 char_range: 37 37 - 2 38 38 - 6 39 - node_id: n0 39 + node_id: p-0-n0 40 40 char_offset_in_node: 2 41 41 child_index: ~ 42 42 utf16_len: 4 ··· 46 46 char_range: 47 47 - 6 48 48 - 8 49 - node_id: n0 49 + node_id: p-0-n0 50 50 char_offset_in_node: 6 51 51 child_index: ~ 52 52 utf16_len: 2 ··· 56 56 char_range: 57 57 - 8 58 58 - 13 59 - node_id: n0 59 + node_id: p-0-n0 60 60 char_offset_in_node: 8 61 61 child_index: ~ 62 62 utf16_len: 5 ··· 66 66 char_range: 67 67 - 13 68 68 - 14 69 - node_id: n0 69 + node_id: p-0-n0 70 70 char_offset_in_node: 13 71 71 child_index: ~ 72 72 utf16_len: 1 ··· 76 76 char_range: 77 77 - 14 78 78 - 20 79 - node_id: n0 79 + node_id: p-0-n0 80 80 char_offset_in_node: 14 81 81 child_index: ~ 82 82 utf16_len: 6 ··· 86 86 char_range: 87 87 - 20 88 88 - 21 89 - node_id: n0 89 + node_id: p-0-n0 90 90 char_offset_in_node: 20 91 91 child_index: ~ 92 92 utf16_len: 1 ··· 96 96 char_range: 97 97 - 21 98 98 - 26 99 - node_id: n0 99 + node_id: p-0-n0 100 100 char_offset_in_node: 21 101 101 child_index: ~ 102 102 utf16_len: 5 ··· 106 106 char_range: 107 107 - 27 108 108 - 31 109 - node_id: n0 109 + node_id: p-0-n0 110 110 char_offset_in_node: 26 111 111 child_index: ~ 112 112 utf16_len: 6
+59 -19
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__nested_list.snap
··· 27 27 char_range: 28 28 - 11 29 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" 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 31 offset_map: 32 32 - byte_range: 33 33 - 0 34 34 - 2 35 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: 36 63 - 11 37 64 - 13 38 - node_id: n0 65 + char_range: 66 + - 11 67 + - 13 68 + node_id: p-0-n1 39 69 char_offset_in_node: 0 40 70 child_index: ~ 41 71 utf16_len: 2 42 72 - byte_range: 43 - - 2 44 - - 9 73 + - 13 74 + - 20 45 75 char_range: 46 76 - 13 47 77 - 20 48 - node_id: n0 78 + node_id: p-0-n1 49 79 char_offset_in_node: 2 50 80 child_index: ~ 51 81 utf16_len: 7 52 82 - byte_range: 53 - - 9 54 - - 12 83 + - 20 84 + - 21 55 85 char_range: 56 86 - 20 57 - - 23 58 - node_id: n0 87 + - 21 88 + node_id: p-0-n1 59 89 char_offset_in_node: 9 60 90 child_index: ~ 61 - utf16_len: 3 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 62 102 - byte_range: 63 - - 12 64 - - 14 103 + - 23 104 + - 25 65 105 char_range: 66 106 - 23 67 107 - 25 68 - node_id: n1 108 + node_id: p-0-n3 69 109 char_offset_in_node: 0 70 110 child_index: ~ 71 111 utf16_len: 2 72 112 - byte_range: 73 - - 14 74 - - 21 113 + - 25 114 + - 32 75 115 char_range: 76 116 - 25 77 117 - 32 78 - node_id: n1 118 + node_id: p-0-n3 79 119 char_offset_in_node: 2 80 120 child_index: ~ 81 121 utf16_len: 7 82 122 - byte_range: 83 - - 21 84 - - 22 123 + - 32 124 + - 33 85 125 char_range: 86 126 - 32 87 127 - 33 88 - node_id: n1 128 + node_id: p-0-n3 89 129 char_offset_in_node: 9 90 130 child_index: ~ 91 131 utf16_len: 1
+9 -9
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__ordered_list.snap
··· 8 8 char_range: 9 9 - 0 10 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" 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 12 offset_map: 13 13 - byte_range: 14 14 - 0 ··· 16 16 char_range: 17 17 - 0 18 18 - 3 19 - node_id: n0 19 + node_id: p-0-n0 20 20 char_offset_in_node: 0 21 21 child_index: ~ 22 22 utf16_len: 3 ··· 26 26 char_range: 27 27 - 3 28 28 - 8 29 - node_id: n0 29 + node_id: p-0-n0 30 30 char_offset_in_node: 3 31 31 child_index: ~ 32 32 utf16_len: 5 ··· 36 36 char_range: 37 37 - 8 38 38 - 9 39 - node_id: n0 39 + node_id: p-0-n0 40 40 char_offset_in_node: 8 41 41 child_index: ~ 42 42 utf16_len: 1 ··· 46 46 char_range: 47 47 - 9 48 48 - 12 49 - node_id: n1 49 + node_id: p-0-n1 50 50 char_offset_in_node: 0 51 51 child_index: ~ 52 52 utf16_len: 3 ··· 56 56 char_range: 57 57 - 12 58 58 - 18 59 - node_id: n1 59 + node_id: p-0-n1 60 60 char_offset_in_node: 3 61 61 child_index: ~ 62 62 utf16_len: 6 ··· 66 66 char_range: 67 67 - 18 68 68 - 19 69 - node_id: n1 69 + node_id: p-0-n1 70 70 char_offset_in_node: 9 71 71 child_index: ~ 72 72 utf16_len: 1 ··· 76 76 char_range: 77 77 - 19 78 78 - 22 79 - node_id: n2 79 + node_id: p-0-n2 80 80 char_offset_in_node: 0 81 81 child_index: ~ 82 82 utf16_len: 3 ··· 86 86 char_range: 87 87 - 22 88 88 - 27 89 - node_id: n2 89 + node_id: p-0-n2 90 90 char_offset_in_node: 3 91 91 child_index: ~ 92 92 utf16_len: 5
+3 -3
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__single_paragraph.snap
··· 8 8 char_range: 9 9 - 0 10 10 - 11 11 - html: "<p id=\"n0\">Hello world</p>\n" 11 + html: "<p id=\"p-0-n0\" dir=\"ltr\">Hello world</p>\n" 12 12 offset_map: 13 13 - byte_range: 14 14 - 0 ··· 16 16 char_range: 17 17 - 0 18 18 - 0 19 - node_id: n0 19 + node_id: p-0-n0 20 20 char_offset_in_node: 0 21 21 child_index: 0 22 22 utf16_len: 0 ··· 26 26 char_range: 27 27 - 0 28 28 - 11 29 - node_id: n0 29 + node_id: p-0-n0 30 30 char_offset_in_node: 0 31 31 child_index: ~ 32 32 utf16_len: 11
+41 -21
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__three_paragraphs.snap
··· 8 8 char_range: 9 9 - 0 10 10 - 5 11 - html: "<p id=\"n0\">One.\n</p>\n" 11 + html: "<p id=\"p-0-n0\" dir=\"ltr\">One.\n</p>\n" 12 12 offset_map: 13 13 - byte_range: 14 14 - 0 ··· 16 16 char_range: 17 17 - 0 18 18 - 0 19 - node_id: n0 19 + node_id: p-0-n0 20 20 char_offset_in_node: 0 21 21 child_index: 0 22 22 utf16_len: 0 ··· 26 26 char_range: 27 27 - 0 28 28 - 4 29 - node_id: n0 29 + node_id: p-0-n0 30 30 char_offset_in_node: 0 31 31 child_index: ~ 32 32 utf16_len: 4 ··· 36 36 char_range: 37 37 - 4 38 38 - 5 39 - node_id: n0 39 + node_id: p-0-n0 40 40 char_offset_in_node: 4 41 41 child_index: ~ 42 42 utf16_len: 1 ··· 47 47 char_range: 48 48 - 6 49 49 - 11 50 - html: "<p id=\"n1\">Two.\n</p>\n" 50 + html: "<span id=\"p-1-n0\">\n</span>\n<p id=\"p-1-n1\" dir=\"ltr\">Two.\n</p>\n" 51 51 offset_map: 52 52 - byte_range: 53 - - 0 54 - - 0 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 55 65 char_range: 56 66 - 6 57 67 - 6 58 - node_id: n1 68 + node_id: p-1-n1 59 69 char_offset_in_node: 0 60 70 child_index: 0 61 71 utf16_len: 0 62 72 - byte_range: 63 - - 0 64 - - 4 73 + - 6 74 + - 10 65 75 char_range: 66 76 - 6 67 77 - 10 68 - node_id: n1 78 + node_id: p-1-n1 69 79 char_offset_in_node: 0 70 80 child_index: ~ 71 81 utf16_len: 4 72 82 - byte_range: 73 - - 4 74 - - 5 83 + - 10 84 + - 11 75 85 char_range: 76 86 - 10 77 87 - 11 78 - node_id: n1 88 + node_id: p-1-n1 79 89 char_offset_in_node: 4 80 90 child_index: ~ 81 91 utf16_len: 1 ··· 86 96 char_range: 87 97 - 12 88 98 - 18 89 - html: "<p id=\"n2\">Three.</p>\n" 99 + html: "<span id=\"p-2-n0\">\n</span>\n<p id=\"p-2-n1\" dir=\"ltr\">Three.</p>\n" 90 100 offset_map: 91 101 - byte_range: 92 - - 0 93 - - 0 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 94 114 char_range: 95 115 - 12 96 116 - 12 97 - node_id: n2 117 + node_id: p-2-n1 98 118 char_offset_in_node: 0 99 119 child_index: 0 100 120 utf16_len: 0 101 121 - byte_range: 102 - - 0 103 - - 6 122 + - 12 123 + - 18 104 124 char_range: 105 125 - 12 106 126 - 18 107 - node_id: n2 127 + node_id: p-2-n1 108 128 char_offset_in_node: 0 109 129 child_index: ~ 110 130 utf16_len: 6
+4 -4
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__trailing_double_newline.snap
··· 8 8 char_range: 9 9 - 0 10 10 - 6 11 - html: "<p id=\"n0\">Hello\n</p>\n" 11 + html: "<p id=\"p-0-n0\" dir=\"ltr\">Hello\n</p>\n" 12 12 offset_map: 13 13 - byte_range: 14 14 - 0 ··· 16 16 char_range: 17 17 - 0 18 18 - 0 19 - node_id: n0 19 + node_id: p-0-n0 20 20 char_offset_in_node: 0 21 21 child_index: 0 22 22 utf16_len: 0 ··· 26 26 char_range: 27 27 - 0 28 28 - 5 29 - node_id: n0 29 + node_id: p-0-n0 30 30 char_offset_in_node: 0 31 31 child_index: ~ 32 32 utf16_len: 5 ··· 36 36 char_range: 37 37 - 5 38 38 - 6 39 - node_id: n0 39 + node_id: p-0-n0 40 40 char_offset_in_node: 5 41 41 child_index: ~ 42 42 utf16_len: 1
+4 -4
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__trailing_single_newline.snap
··· 8 8 char_range: 9 9 - 0 10 10 - 6 11 - html: "<p id=\"n0\">Hello\n</p>\n" 11 + html: "<p id=\"p-0-n0\" dir=\"ltr\">Hello\n</p>\n" 12 12 offset_map: 13 13 - byte_range: 14 14 - 0 ··· 16 16 char_range: 17 17 - 0 18 18 - 0 19 - node_id: n0 19 + node_id: p-0-n0 20 20 char_offset_in_node: 0 21 21 child_index: 0 22 22 utf16_len: 0 ··· 26 26 char_range: 27 27 - 0 28 28 - 5 29 - node_id: n0 29 + node_id: p-0-n0 30 30 char_offset_in_node: 0 31 31 child_index: ~ 32 32 utf16_len: 5 ··· 36 36 char_range: 37 37 - 5 38 38 - 6 39 - node_id: n0 39 + node_id: p-0-n0 40 40 char_offset_in_node: 5 41 41 child_index: ~ 42 42 utf16_len: 1
+21 -11
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__two_paragraphs.snap
··· 8 8 char_range: 9 9 - 0 10 10 - 17 11 - html: "<p id=\"n0\">First paragraph.\n</p>\n" 11 + html: "<p id=\"p-0-n0\" dir=\"ltr\">First paragraph.\n</p>\n" 12 12 offset_map: 13 13 - byte_range: 14 14 - 0 ··· 16 16 char_range: 17 17 - 0 18 18 - 0 19 - node_id: n0 19 + node_id: p-0-n0 20 20 char_offset_in_node: 0 21 21 child_index: 0 22 22 utf16_len: 0 ··· 26 26 char_range: 27 27 - 0 28 28 - 16 29 - node_id: n0 29 + node_id: p-0-n0 30 30 char_offset_in_node: 0 31 31 child_index: ~ 32 32 utf16_len: 16 ··· 36 36 char_range: 37 37 - 16 38 38 - 17 39 - node_id: n0 39 + node_id: p-0-n0 40 40 char_offset_in_node: 16 41 41 child_index: ~ 42 42 utf16_len: 1 ··· 47 47 char_range: 48 48 - 18 49 49 - 35 50 - html: "<p id=\"n1\">Second paragraph.</p>\n" 50 + html: "<span id=\"p-1-n0\">\n</span>\n<p id=\"p-1-n1\" dir=\"ltr\">Second paragraph.</p>\n" 51 51 offset_map: 52 52 - byte_range: 53 - - 0 54 - - 0 53 + - 17 54 + - 18 55 55 char_range: 56 + - 17 56 57 - 18 58 + node_id: p-1-n0 59 + char_offset_in_node: 0 60 + child_index: ~ 61 + utf16_len: 1 62 + - byte_range: 57 63 - 18 58 - node_id: n1 64 + - 18 65 + char_range: 66 + - 18 67 + - 18 68 + node_id: p-1-n1 59 69 char_offset_in_node: 0 60 70 child_index: 0 61 71 utf16_len: 0 62 72 - byte_range: 63 - - 0 64 - - 17 73 + - 18 74 + - 35 65 75 char_range: 66 76 - 18 67 77 - 35 68 - node_id: n1 78 + node_id: p-1-n1 69 79 char_offset_in_node: 0 70 80 child_index: ~ 71 81 utf16_len: 17
+3 -3
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__unicode_cjk.snap
··· 8 8 char_range: 9 9 - 0 10 10 - 4 11 - html: "<p id=\"n0\">你好世界</p>\n" 11 + html: "<p id=\"p-0-n0\" dir=\"ltr\">你好世界</p>\n" 12 12 offset_map: 13 13 - byte_range: 14 14 - 0 ··· 16 16 char_range: 17 17 - 0 18 18 - 0 19 - node_id: n0 19 + node_id: p-0-n0 20 20 char_offset_in_node: 0 21 21 child_index: 0 22 22 utf16_len: 0 ··· 26 26 char_range: 27 27 - 0 28 28 - 4 29 - node_id: n0 29 + node_id: p-0-n0 30 30 char_offset_in_node: 0 31 31 child_index: ~ 32 32 utf16_len: 4
+3 -3
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__unicode_emoji.snap
··· 8 8 char_range: 9 9 - 0 10 10 - 13 11 - html: "<p id=\"n0\">Hello 🎉 world</p>\n" 11 + html: "<p id=\"p-0-n0\" dir=\"ltr\">Hello 🎉 world</p>\n" 12 12 offset_map: 13 13 - byte_range: 14 14 - 0 ··· 16 16 char_range: 17 17 - 0 18 18 - 0 19 - node_id: n0 19 + node_id: p-0-n0 20 20 char_offset_in_node: 0 21 21 child_index: 0 22 22 utf16_len: 0 ··· 26 26 char_range: 27 27 - 0 28 28 - 13 29 - node_id: n0 29 + node_id: p-0-n0 30 30 char_offset_in_node: 0 31 31 child_index: ~ 32 32 utf16_len: 14
+9 -9
crates/weaver-app/src/components/editor/snapshots/weaver_app__components__editor__tests__unordered_list.snap
··· 8 8 char_range: 9 9 - 0 10 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" 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 12 offset_map: 13 13 - byte_range: 14 14 - 0 ··· 16 16 char_range: 17 17 - 0 18 18 - 2 19 - node_id: n0 19 + node_id: p-0-n0 20 20 char_offset_in_node: 0 21 21 child_index: ~ 22 22 utf16_len: 2 ··· 26 26 char_range: 27 27 - 2 28 28 - 8 29 - node_id: n0 29 + node_id: p-0-n0 30 30 char_offset_in_node: 2 31 31 child_index: ~ 32 32 utf16_len: 6 ··· 36 36 char_range: 37 37 - 8 38 38 - 9 39 - node_id: n0 39 + node_id: p-0-n0 40 40 char_offset_in_node: 8 41 41 child_index: ~ 42 42 utf16_len: 1 ··· 46 46 char_range: 47 47 - 9 48 48 - 11 49 - node_id: n1 49 + node_id: p-0-n1 50 50 char_offset_in_node: 0 51 51 child_index: ~ 52 52 utf16_len: 2 ··· 56 56 char_range: 57 57 - 11 58 58 - 17 59 - node_id: n1 59 + node_id: p-0-n1 60 60 char_offset_in_node: 2 61 61 child_index: ~ 62 62 utf16_len: 6 ··· 66 66 char_range: 67 67 - 17 68 68 - 18 69 - node_id: n1 69 + node_id: p-0-n1 70 70 char_offset_in_node: 8 71 71 child_index: ~ 72 72 utf16_len: 1 ··· 76 76 char_range: 77 77 - 18 78 78 - 20 79 - node_id: n2 79 + node_id: p-0-n2 80 80 char_offset_in_node: 0 81 81 child_index: ~ 82 82 utf16_len: 2 ··· 86 86 char_range: 87 87 - 20 88 88 - 26 89 - node_id: n2 89 + node_id: p-0-n2 90 90 char_offset_in_node: 2 91 91 child_index: ~ 92 92 utf16_len: 6
+61 -4
crates/weaver-app/src/components/editor/tests.rs
··· 726 726 char_range: 727 727 - 0 728 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" 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 730 offset_map: 731 731 - byte_range: 732 732 - 1 ··· 734 734 char_range: 735 735 - 0 736 736 - 0 737 - node_id: n0 737 + node_id: p-0-n0 738 738 char_offset_in_node: 0 739 739 child_index: 0 740 740 utf16_len: 0 ··· 744 744 char_range: 745 745 - 0 746 746 - 1 747 - node_id: n0 747 + node_id: p-0-n0 748 748 char_offset_in_node: 0 749 749 child_index: ~ 750 750 utf16_len: 1 ··· 754 754 char_range: 755 755 - 1 756 756 - 6 757 - node_id: n0 757 + node_id: p-0-n0 758 758 char_offset_in_node: 1 759 759 child_index: ~ 760 760 utf16_len: 5 ··· 1089 1089 ); 1090 1090 } 1091 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 10 pub mod syntax; 11 11 pub mod tags; 12 12 13 + use super::offset_map::OffsetMapping; 13 14 use crate::components::editor::writer::segmented::SegmentedWriter; 14 15 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 16 use loro::LoroText; 20 17 use markdown_weaver::{Alignment, CowStr, Event, WeaverAttributes}; 21 18 use markdown_weaver_escape::{StrWrite, escape_html, escape_html_body_text_with_char_count}; 22 19 use std::collections::HashMap; 23 20 use std::fmt; 24 21 use std::ops::Range; 22 + pub use syntax::{SyntaxSpanInfo, SyntaxType}; 25 23 use weaver_common::EntryIndex; 26 24 27 25 /// Result of rendering with the EditorWriter. ··· 117 115 paragraph_ranges: Vec<(Range<usize>, Range<usize>)>, // (byte_range, char_range) 118 116 current_paragraph_start: Option<(usize, usize)>, // (byte_offset, char_offset) 119 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 120 119 121 120 // Syntax span tracking for conditional visibility - current paragraph 122 121 syntax_spans: Vec<SyntaxSpanInfo>, ··· 157 156 { 158 157 pub fn new(source: &'a str, source_text: &'a LoroText, events: I) -> Self { 159 158 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 159 } 314 160 315 161 /// Get the next paragraph ID that would be assigned (for tracking allocations). ··· 1139 985 1140 986 // Emit space for cursor positioning - this gives the browser somewhere 1141 987 // to place the cursor when navigating to this line 1142 - self.write(" ")?; 988 + self.write("\u{200B}")?; 1143 989 self.current_node_child_count += 1; 1144 990 1145 991 // Map the space to the newline position - cursor landing here means ··· 1193 1039 self.current_node_child_count += 1; 1194 1040 1195 1041 // After <br>, emit plain zero-width space for cursor positioning 1196 - self.write(" ")?; 1042 + self.write("\u{200B}")?; 1197 1043 1198 1044 // Count the zero-width space text node as a child 1199 1045 self.current_node_child_count += 1; ··· 1215 1061 self.current_node_char_offset += 1; 1216 1062 } 1217 1063 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 1064 self.last_char_offset = char_start + spaces_char_len + 1; // +1 for \n 1221 1065 } else { 1222 1066 // Fallback: just <br> ··· 1259 1103 self.write("<div class=\"toggle-block\"><hr /></div>\n")?; 1260 1104 } 1261 1105 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 1106 + // Emit [^name] as styled (but NOT hidden) inline span 1267 1107 let raw_text = &self.source[range.clone()]; 1268 1108 let char_start = self.last_char_offset; 1269 1109 let syntax_char_len = raw_text.chars().count(); 1270 1110 let char_end = char_start + syntax_char_len; 1271 - let syn_id = self.gen_syn_id(); 1272 1111 1112 + // Use footnote-ref class for styling, not md-syntax-inline (which hides) 1273 1113 write!( 1274 1114 &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 1115 + "<span class=\"footnote-ref\" data-char-start=\"{}\" data-char-end=\"{}\" data-footnote=\"{}\">", 1116 + char_start, char_end, name 1277 1117 )?; 1278 1118 escape_html(&mut self.writer, raw_text)?; 1279 1119 self.write("</span>")?; 1280 1120 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 1121 + // Record offset mapping 1293 1122 self.record_mapping(range.clone(), char_start..char_end); 1294 1123 1295 1124 // Count as child 1296 1125 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 1126 1305 1127 // Update tracking 1306 1128 self.last_char_offset = char_end; ··· 1348 1170 } 1349 1171 } 1350 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 1351 1328 } 1352 1329 }
+116 -71
crates/weaver-app/src/components/editor/writer/tags.rs
··· 16 16 impl<'a, I: Iterator<Item = (Event<'a>, Range<usize>)>, E: EmbedContentProvider, R: ImageResolver> 17 17 EditorWriter<'a, I, E, R> 18 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 + 19 32 pub(crate) fn start_tag( 20 33 &mut self, 21 34 tag: Tag<'_>, ··· 128 141 // HTML blocks get their own paragraph to try and corral them better 129 142 Tag::HtmlBlock => { 130 143 // Record paragraph start for boundary tracking 131 - // BUT skip if inside a list - list owns the paragraph boundary 132 - if self.list_depth == 0 { 144 + // Skip if inside a list or footnote def - they own their paragraph boundary 145 + if self.list_depth == 0 && !self.in_footnote_def { 133 146 self.current_paragraph_start = 134 147 Some((self.last_byte_offset, self.last_char_offset)); 135 148 } ··· 170 183 self.emit_wrapper_start()?; 171 184 172 185 // Record paragraph start for boundary tracking 173 - // BUT skip if inside a list - list owns the paragraph boundary 174 - if self.list_depth == 0 { 186 + // Skip if inside a list or footnote def - they own their paragraph boundary 187 + if self.list_depth == 0 && !self.in_footnote_def { 175 188 self.current_paragraph_start = 176 189 Some((self.last_byte_offset, self.last_char_offset)); 177 190 } 178 191 179 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 + 180 197 if self.end_newline { 181 - write!(&mut self.writer, "<p id=\"{}\">", node_id)?; 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 + } 182 203 } else { 183 - write!(&mut self.writer, "\n<p id=\"{}\">", node_id)?; 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 + } 184 209 } 185 210 self.begin_node(node_id.clone()); 186 211 ··· 244 269 // Generate node ID for offset tracking 245 270 let node_id = self.gen_node_id(); 246 271 272 + // Detect text direction for this heading 273 + let dir = self.detect_paragraph_direction(self.last_byte_offset); 274 + 247 275 self.write("<")?; 248 276 write!(&mut self.writer, "{}", level)?; 249 277 ··· 267 295 } 268 296 self.write("\"")?; 269 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 + 270 306 for (attr, value) in attrs { 271 307 self.write(" ")?; 272 308 escape_html(&mut self.writer, &attr)?; ··· 868 904 Ok(()) 869 905 } 870 906 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(); 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; 878 911 879 912 if !self.end_newline { 880 913 self.write("\n")?; 881 914 } 882 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 883 921 write!( 884 922 &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 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 887 968 )?; 888 - escape_html(&mut self.writer, &prefix)?; 969 + escape_html(&mut self.writer, prefix)?; 889 970 self.write("</span>")?; 890 971 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)); 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)); 902 974 903 - // Record offset mapping for the syntax span 975 + // Record offset mapping for the prefix 904 976 self.record_mapping( 905 - range.start..range.start + prefix.len(), 977 + range.start..range.start + prefix_byte_len, 906 978 char_start..char_end, 907 979 ); 908 980 909 981 // Update tracking for the prefix 910 982 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 - )?; 983 + self.last_byte_offset = range.start + prefix_byte_len; 928 984 929 985 Ok(()) 930 986 } ··· 946 1002 let result = match tag { 947 1003 TagEnd::HtmlBlock => { 948 1004 // 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 { 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 { 951 1007 self.current_paragraph_start 952 1008 .take() 953 1009 .map(|(byte_start, char_start)| { ··· 972 1028 } 973 1029 TagEnd::Paragraph(_) => { 974 1030 // 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 { 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 { 977 1033 self.current_paragraph_start 978 1034 .take() 979 1035 .map(|(byte_start, char_start)| { ··· 1418 1474 Ok(()) 1419 1475 } 1420 1476 TagEnd::FootnoteDefinition => { 1477 + // End node tracking (inner paragraphs may have already cleared it) 1478 + self.end_node(); 1421 1479 self.write("</div>\n")?; 1422 1480 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; 1481 + // Clear footnote tracking 1482 + self.current_footnote_def.take(); 1483 + self.in_footnote_def = false; 1428 1484 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 - } 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); 1445 1490 } 1446 1491 1447 1492 Ok(())
-1
crates/weaver-app/src/lib.rs
··· 1 1 //! Weaver App library. 2 - 3 2 #[allow(unused)] 4 3 use dioxus::{CapturedError, prelude::*}; 5 4
+3 -2
crates/weaver-app/src/main.rs
··· 29 29 use tracing_subscriber::layer::SubscriberExt; 30 30 31 31 let console_level = if cfg!(debug_assertions) { 32 - Level::DEBUG 32 + Level::TRACE 33 33 } else { 34 34 Level::INFO 35 35 }; ··· 41 41 ); 42 42 43 43 // Filter out noisy crates 44 + // Use weaver_app=trace for detailed editor debugging 44 45 let filter = EnvFilter::new( 45 - "debug,loro_internal=warn,jacquard_identity=info,jacquard_common=info,iroh=info", 46 + "debug,weaver_app=trace,loro_internal=warn,jacquard_identity=info,jacquard_common=info,iroh=info", 46 47 ); 47 48 48 49 let reg = Registry::default()
+5 -3
crates/weaver-common/src/agent.rs
··· 17 17 use jacquard::prelude::*; 18 18 use jacquard::smol_str::SmolStr; 19 19 use jacquard::types::blob::{BlobRef, MimeType}; 20 - use jacquard::types::string::{AtUri, Did, RecordKey, Rkey}; 20 + use jacquard::types::string::{AtUri, Datetime, Did, RecordKey, Rkey}; 21 21 #[allow(unused_imports)] 22 22 use jacquard::types::tid::Tid; 23 23 use jacquard::types::uri::Uri; ··· 138 138 async move { 139 139 use weaver_api::sh_weaver::notebook::book::Book; 140 140 141 - let at_uri = AtUri::new(uri) 142 - .map_err(|e| WeaverError::InvalidNotebook(format!("Invalid notebook URI: {}", e)))?; 141 + let at_uri = AtUri::new(uri).map_err(|e| { 142 + WeaverError::InvalidNotebook(format!("Invalid notebook URI: {}", e)) 143 + })?; 143 144 144 145 let response = match self.get_record::<Book>(&at_uri).await { 145 146 Ok(r) => r, ··· 352 353 e.content = entry.content.clone(); 353 354 e.embeds = entry.embeds.clone(); 354 355 e.tags = entry.tags.clone(); 356 + e.updated_at = Some(Datetime::now()); 355 357 }) 356 358 .await?; 357 359 let updated_ref = StrongRef::new()
+12 -9
crates/weaver-index/src/clickhouse/queries/edit.rs
··· 208 208 /// 209 209 /// Compares edit_heads to draft_titles to find drafts where the current 210 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> { 211 + pub async fn get_stale_draft_titles( 212 + &self, 213 + limit: i64, 214 + ) -> Result<Vec<StaleDraftRow>, IndexError> { 212 215 // Join drafts -> edit_heads (for current head) -> draft_titles (to check staleness) 213 216 // edit_heads uses resource_type='draft' and resource_did/resource_rkey to link 214 217 let query = r#" 215 218 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 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 224 227 FROM drafts d FINAL 225 228 INNER JOIN edit_heads h FINAL 226 229 ON h.resource_did = d.did
+1
crates/weaver-renderer/Cargo.toml
··· 19 19 tracing.workspace = true 20 20 miette.workspace = true 21 21 unicode-normalization = "0.1.24" 22 + unicode-bidi = "0.3" 22 23 yaml-rust2 = { version = "0.10.2" } 23 24 bitflags = "2.9.1" 24 25 dashmap = "6.1.0"
+2 -2
crates/weaver-renderer/src/atproto/snapshots/weaver_renderer__atproto__tests__blockquote_rendering.snap
··· 3 3 expression: output 4 4 --- 5 5 <blockquote> 6 - <p>This is a quote</p> 7 - <p>With multiple lines</p> 6 + <p dir="ltr">This is a quote</p> 7 + <p dir="ltr">With multiple lines</p> 8 8 </blockquote>
+2 -2
crates/weaver-renderer/src/atproto/snapshots/weaver_renderer__atproto__tests__footnote_in_blockquote.snap
··· 3 3 expression: output 4 4 --- 5 5 <blockquote> 6 - <p>Quote with footnote<sup class="footnote-reference"><a href="#q">1</a></sup>.</p> 6 + <p dir="ltr">Quote with footnote<sup class="footnote-reference"><a href="#q">1</a></sup>.</p> 7 7 </blockquote> 8 8 <div class="footnote-definition" id="q"><sup class="footnote-definition-label">1</sup> 9 - <p>Footnote for quote.</p> 9 + <p dir="ltr">Footnote for quote.</p> 10 10 </div>
+3 -3
crates/weaver-renderer/src/atproto/snapshots/weaver_renderer__atproto__tests__footnote_multiple.snap
··· 2 2 source: crates/weaver-renderer/src/atproto/tests.rs 3 3 expression: output 4 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> 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 6 <div class="footnote-definition" id="1"><sup class="footnote-definition-label">1</sup> 7 - <p>First note.</p> 7 + <p dir="ltr">First note.</p> 8 8 </div> 9 9 <div class="footnote-definition" id="2"><sup class="footnote-definition-label">2</sup> 10 - <p>Second note.</p> 10 + <p dir="ltr">Second note.</p> 11 11 </div>
+1 -1
crates/weaver-renderer/src/atproto/snapshots/weaver_renderer__atproto__tests__footnote_named.snap
··· 2 2 source: crates/weaver-renderer/src/atproto/tests.rs 3 3 expression: output 4 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> 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 2 source: crates/weaver-renderer/src/atproto/tests.rs 3 3 expression: output 4 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> 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 2 source: crates/weaver-renderer/src/atproto/tests.rs 3 3 expression: output 4 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> 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 2 source: crates/weaver-renderer/src/atproto/tests.rs 3 3 expression: output 4 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> 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 2 source: crates/weaver-renderer/src/atproto/tests.rs 3 3 expression: output 4 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> 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 2 source: crates/weaver-renderer/src/atproto/tests.rs 3 3 expression: output 4 4 --- 5 - <p>This is a paragraph.</p> 6 - <p>This is another paragraph.</p> 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 3 expression: output 4 4 --- 5 5 <aside> 6 - <p>This paragraph should be in an aside.</p> 6 + <p dir="ltr">This paragraph should be in an aside.</p> 7 7 </aside>
+1 -1
crates/weaver-renderer/src/atproto/snapshots/weaver_renderer__atproto__tests__weaver_block_before_blockquote.snap
··· 4 4 --- 5 5 <aside> 6 6 <blockquote> 7 - <p>This blockquote is in an aside.</p> 7 + <p dir="ltr">This blockquote is in an aside.</p> 8 8 </aside> 9 9 </blockquote>
+1 -1
crates/weaver-renderer/src/atproto/snapshots/weaver_renderer__atproto__tests__weaver_block_before_heading.snap
··· 4 4 --- 5 5 <aside> 6 6 <h2>Heading in aside</h2> 7 - <p>Paragraph also in aside.</p> 7 + <p dir="ltr">Paragraph also in aside.</p> 8 8 </aside>
+1 -1
crates/weaver-renderer/src/atproto/snapshots/weaver_renderer__atproto__tests__weaver_block_custom_attributes.snap
··· 3 3 expression: output 4 4 --- 5 5 <div class="foo" width="300px" data-test="value"> 6 - <p>Paragraph with class and attributes.</p> 6 + <p dir="ltr">Paragraph with class and attributes.</p> 7 7 </div>
+1 -1
crates/weaver-renderer/src/atproto/snapshots/weaver_renderer__atproto__tests__weaver_block_custom_class.snap
··· 3 3 expression: output 4 4 --- 5 5 <div class="highlight"> 6 - <p>This paragraph has a custom class.</p> 6 + <p dir="ltr">This paragraph has a custom class.</p> 7 7 </div>
+1 -1
crates/weaver-renderer/src/atproto/snapshots/weaver_renderer__atproto__tests__weaver_block_multiple_classes.snap
··· 3 3 expression: output 4 4 --- 5 5 <aside class="highlight important"> 6 - <p>Multiple classes applied.</p> 6 + <p dir="ltr">Multiple classes applied.</p> 7 7 </aside>
+2 -2
crates/weaver-renderer/src/atproto/snapshots/weaver_renderer__atproto__tests__weaver_block_no_effect_on_following.snap
··· 3 3 expression: output 4 4 --- 5 5 <aside> 6 - <p>First paragraph in aside.</p> 6 + <p dir="ltr">First paragraph in aside.</p> 7 7 </aside> 8 - <p>Second paragraph NOT in aside.</p> 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 3 expression: output 4 4 --- 5 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> 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 7 </aside>
+33 -4
crates/weaver-renderer/src/atproto/writer.rs
··· 92 92 in_sidenote: bool, 93 93 /// Whether we're deferring paragraph close for sidenote handling 94 94 defer_paragraph_close: bool, 95 + /// Buffered paragraph opening tag (without closing `>`) for dir attribute emission 96 + pending_paragraph_open: Option<String>, 95 97 96 98 _phantom: std::marker::PhantomData<&'a ()>, 97 99 } ··· 126 128 pending_footnote_content: self.pending_footnote_content, 127 129 in_sidenote: self.in_sidenote, 128 130 defer_paragraph_close: self.defer_paragraph_close, 131 + pending_paragraph_open: self.pending_paragraph_open, 129 132 _phantom: std::marker::PhantomData, 130 133 } 131 134 } ··· 153 156 pending_footnote_content: String::new(), 154 157 in_sidenote: false, 155 158 defer_paragraph_close: false, 159 + pending_paragraph_open: None, 156 160 _phantom: std::marker::PhantomData, 157 161 } 158 162 } ··· 191 195 /// Close deferred paragraph if we're in that state. 192 196 /// Called when a non-paragraph block element starts. 193 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 + } 194 203 if self.defer_paragraph_close { 195 204 // Flush pending footnote as traditional before closing 196 205 self.flush_pending_footnote()?; ··· 397 406 // Buffer text while waiting to see if footnote def follows 398 407 self.pending_footnote_content.push_str(&text); 399 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 + } 400 421 escape_html_body_text(&mut self.writer, &text)?; 401 422 self.end_newline = text.ends_with('\n'); 402 423 } ··· 495 516 } else { 496 517 self.flush_pending_footnote()?; 497 518 self.emit_wrapper_start()?; 498 - if self.end_newline { 499 - self.write("<p>") 519 + // Buffer paragraph opening for dir attribute detection 520 + let opening = if self.end_newline { 521 + String::from("<p") 500 522 } else { 501 - self.write("\n<p>") 502 - } 523 + String::from("\n<p") 524 + }; 525 + self.pending_paragraph_open = Some(opening); 526 + Ok(()) 503 527 } 504 528 } 505 529 Tag::Heading { ··· 857 881 self.defer_paragraph_close = false; 858 882 Ok(()) 859 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 + } 860 889 self.write("</p>\n")?; 861 890 self.close_wrapper() 862 891 }
+27 -4
crates/weaver-renderer/src/base_html.rs
··· 38 38 table_alignments: Vec<Alignment>, 39 39 table_cell_index: usize, 40 40 numbers: HashMap<CowStr<'a>, usize>, 41 + /// Buffered paragraph opening tag (without closing `>`) for dir attribute emission 42 + pending_paragraph_open: Option<String>, 41 43 } 42 44 43 45 impl<'a, I, W> HtmlWriter<'a, I, W> ··· 55 57 table_alignments: vec![], 56 58 table_cell_index: 0, 57 59 numbers: HashMap::new(), 60 + pending_paragraph_open: None, 58 61 } 59 62 } 60 63 ··· 87 90 } 88 91 Text(text) => { 89 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 + } 90 105 escape_html_body_text(&mut self.writer, &text)?; 91 106 self.end_newline = text.ends_with('\n'); 92 107 } ··· 158 173 match tag { 159 174 Tag::HtmlBlock => Ok(()), 160 175 Tag::Paragraph(_) => { 161 - if self.end_newline { 162 - self.write("<p>") 176 + // Buffer paragraph opening for dir attribute detection 177 + let opening = if self.end_newline { 178 + String::from("<p") 163 179 } else { 164 - self.write("\n<p>") 165 - } 180 + String::from("\n<p") 181 + }; 182 + self.pending_paragraph_open = Some(opening); 183 + Ok(()) 166 184 } 167 185 Tag::Heading { 168 186 level, ··· 458 476 match tag { 459 477 TagEnd::HtmlBlock => {} 460 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 + } 461 484 self.write("</p>\n")?; 462 485 } 463 486 TagEnd::Heading(level) => {
+42 -42
crates/weaver-renderer/src/css.rs
··· 132 132 /* When sidenotes exist, body padding creates the gutter */ 133 133 /* Left padding shrinks first as viewport narrows, right stays for sidenotes */ 134 134 body:has(.sidenote) {{ 135 - padding-left: clamp(0rem, calc((100vw - 95ch - 15.5rem - 2rem) / 2), 15.5rem); 136 - padding-right: 15.5rem; 135 + padding-inline-start: clamp(0rem, calc((100vw - 95ch - 15.5rem - 2rem) / 2), 15.5rem); 136 + padding-inline-end: 15.5rem; 137 137 }} 138 138 139 139 /* Typography */ ··· 202 202 203 203 /* Lists */ 204 204 ul, ol {{ 205 - margin-left: 1rem; 205 + margin-inline-start: 1rem; 206 206 margin-bottom: 1rem; 207 207 }} 208 208 ··· 250 250 251 251 /* Blockquotes */ 252 252 blockquote {{ 253 - border-left: 2px solid var(--color-secondary); 253 + border-inline-start: 2px solid var(--color-secondary); 254 254 background: var(--color-surface); 255 - padding-left: 1rem; 256 - padding-right: 1rem; 255 + padding-inline-start: 1rem; 256 + padding-inline-end: 1rem; 257 257 padding-top: 0.5rem; 258 258 padding-bottom: 0.04rem; 259 259 margin: 1rem 0; ··· 276 276 th, td {{ 277 277 border: 1px solid var(--color-border); 278 278 padding: 0.5rem; 279 - text-align: left; 279 + text-align: start; 280 280 }} 281 281 282 282 th {{ ··· 318 318 319 319 .footnote-definition-label {{ 320 320 font-weight: 600; 321 - margin-right: 0.5rem; 321 + margin-inline-end: 0.5rem; 322 322 color: var(--color-primary); 323 323 }} 324 324 325 325 /* Aside blocks (via WeaverBlock prefix) - scoped to notebook content */ 326 326 .notebook-content aside, 327 327 .notebook-content .aside {{ 328 - float: left; 328 + float: inline-start; 329 329 width: 40%; 330 330 margin: 0 1.5rem 1rem 0; 331 331 padding: 1rem; 332 332 background: var(--color-surface); 333 - border-right: 3px solid var(--color-primary); 333 + border-inline-end: 3px solid var(--color-primary); 334 334 font-size: 0.9em; 335 - clear: left; 335 + clear: inline-start; 336 336 }} 337 337 338 338 .notebook-content aside > *:first-child, ··· 348 348 /* Reset blockquote styling inside asides */ 349 349 .notebook-content aside > blockquote, 350 350 .notebook-content .aside > blockquote {{ 351 - border-left: none; 351 + border-inline-start: none; 352 352 background: transparent; 353 353 padding: 0; 354 354 margin: 0; ··· 356 356 }} 357 357 358 358 /* Indent utilities */ 359 - .indent-1 {{ margin-left: 1em; }} 360 - .indent-2 {{ margin-left: 2em; }} 361 - .indent-3 {{ margin-left: 3em; }} 359 + .indent-1 {{ margin-inline-start: 1em; }} 360 + .indent-2 {{ margin-inline-start: 2em; }} 361 + .indent-3 {{ margin-inline-start: 3em; }} 362 362 363 363 /* Tufte-style Sidenotes */ 364 364 /* Hide checkbox for sidenote toggle */ ··· 377 377 position: relative; 378 378 top: -0.5em; 379 379 color: var(--color-primary); 380 - padding-left: 0.1em; 380 + padding-inline-start: 0.1em; 381 381 }} 382 382 383 383 /* Sidenote content (margin notes on wide screens) */ 384 384 .sidenote {{ 385 - float: right; 386 - clear: right; 387 - margin-right: -15.5rem; 385 + float: inline-end; 386 + clear: inline-end; 387 + margin-inline-end: -15.5rem; 388 388 width: 14rem; 389 389 margin-top: 0.3rem; 390 390 margin-bottom: 1rem; ··· 402 402 @media (max-width: 900px) {{ 403 403 /* Reset sidenote gutter on mobile */ 404 404 body:has(.sidenote) {{ 405 - padding-right: 0; 405 + padding-inline-end: 0; 406 406 }} 407 407 408 408 aside, .aside {{ ··· 422 422 margin: 0.5rem 2.5%; 423 423 padding: 0.5rem; 424 424 background: var(--color-surface); 425 - border-left: 2px solid var(--color-primary); 425 + border-inline-start: 2px solid var(--color-primary); 426 426 }} 427 427 428 428 label.sidenote-number {{ ··· 460 460 margin: 1rem 0; 461 461 padding: 1rem; 462 462 background: var(--color-surface); 463 - border-left: 2px solid var(--color-secondary); 463 + border-inline-start: 2px solid var(--color-secondary); 464 464 box-shadow: 0 1px 2px color-mix(in srgb, var(--color-text) 8%, transparent); 465 465 }} 466 466 467 467 .atproto-embed:hover {{ 468 - border-left-color: var(--color-primary); 468 + border-inline-start-color: var(--color-primary); 469 469 }} 470 470 471 471 @media (prefers-color-scheme: dark) {{ 472 472 .atproto-embed {{ 473 473 box-shadow: none; 474 474 border: 1px solid var(--color-border); 475 - border-left: 2px solid var(--color-secondary); 475 + border-inline-start: 2px solid var(--color-secondary); 476 476 }} 477 477 }} 478 478 ··· 649 649 }} 650 650 651 651 .embed-external:hover {{ 652 - border-left: 2px solid var(--color-primary); 653 - margin-left: -1px; 652 + border-inline-start: 2px solid var(--color-primary); 653 + margin-inline-start: -1px; 654 654 }} 655 655 656 656 @media (prefers-color-scheme: dark) {{ ··· 659 659 }} 660 660 661 661 .embed-external:hover {{ 662 - border-left: 2px solid var(--color-primary); 663 - margin-left: -1px; 662 + border-inline-start: 2px solid var(--color-primary); 663 + margin-inline-start: -1px; 664 664 }} 665 665 }} 666 666 ··· 746 746 margin-top: 0.5rem; 747 747 padding: 0.75rem; 748 748 background: var(--color-overlay); 749 - border-left: 2px solid var(--color-tertiary); 749 + border-inline-start: 2px solid var(--color-tertiary); 750 750 }} 751 751 752 752 @media (prefers-color-scheme: dark) {{ 753 753 .embed-quote {{ 754 754 border: 1px solid var(--color-border); 755 - border-left: 2px solid var(--color-tertiary); 755 + border-inline-start: 2px solid var(--color-tertiary); 756 756 }} 757 757 }} 758 758 ··· 783 783 display: block; 784 784 padding: 1rem; 785 785 background: var(--color-overlay); 786 - border-left: 2px solid var(--color-border); 786 + border-inline-start: 2px solid var(--color-border); 787 787 color: var(--color-muted); 788 788 font-style: italic; 789 789 margin-top: 0.5rem; ··· 807 807 margin-top: 0.5rem; 808 808 padding: 0.75rem; 809 809 background: var(--color-overlay); 810 - border-left: 2px solid var(--color-tertiary); 810 + border-inline-start: 2px solid var(--color-tertiary); 811 811 }} 812 812 813 813 .embed-record-card > .embed-author-name {{ ··· 850 850 .embed-fields .embed-fields {{ 851 851 display: block; 852 852 margin-top: 0.5rem; 853 - margin-left: 1rem; 854 - padding-left: 0.5rem; 855 - border-left: 1px solid var(--color-border); 853 + margin-inline-start: 1rem; 854 + padding-inline-start: 0.5rem; 855 + border-inline-start: 1px solid var(--color-border); 856 856 }} 857 857 858 858 /* Type label inside fields should be block with spacing */ ··· 967 967 padding: 0; 968 968 background: var(--color-surface); 969 969 border: 1px solid var(--color-border); 970 - border-left: 1px solid var(--color-border); 970 + border-inline-start: 1px solid var(--color-border); 971 971 box-shadow: none; 972 972 overflow: hidden; 973 973 }} 974 974 975 975 .atproto-entry:hover {{ 976 - border-left-color: var(--color-border); 976 + border-inline-start-color: var(--color-border); 977 977 }} 978 978 979 979 @media (prefers-color-scheme: dark) {{ 980 980 .atproto-entry {{ 981 981 border: 1px solid var(--color-border); 982 - border-left: 1px solid var(--color-border); 982 + border-inline-start: 1px solid var(--color-border); 983 983 }} 984 984 }} 985 985 ··· 1075 1075 h3 {{ font-size: 1.2rem; }} 1076 1076 1077 1077 blockquote {{ 1078 - margin-left: 0; 1079 - margin-right: 0; 1078 + margin-inline-start: 0; 1079 + margin-inline-end: 0; 1080 1080 }} 1081 1081 }} 1082 1082 ··· 1091 1091 h3 {{ font-size: 1.1rem; }} 1092 1092 1093 1093 blockquote {{ 1094 - padding-left: 0.75rem; 1095 - padding-right: 0.75rem; 1094 + padding-inline-start: 0.75rem; 1095 + padding-inline-end: 0.75rem; 1096 1096 }} 1097 1097 }} 1098 1098 "#,
+2 -2
crates/weaver-renderer/src/static_site/snapshots/weaver_renderer__static_site__tests__blockquote_rendering.snap
··· 3 3 expression: output 4 4 --- 5 5 <blockquote> 6 - <p>This is a quote</p> 7 - <p>With multiple lines</p> 6 + <p dir="ltr">This is a quote</p> 7 + <p dir="ltr">With multiple lines</p> 8 8 </blockquote>
+2 -2
crates/weaver-renderer/src/static_site/snapshots/weaver_renderer__static_site__tests__footnote_in_blockquote.snap
··· 3 3 expression: output 4 4 --- 5 5 <blockquote> 6 - <p>Quote with footnote<sup class="footnote-reference"><a href="#q">1</a></sup>.</p> 6 + <p dir="ltr">Quote with footnote<sup class="footnote-reference"><a href="#q">1</a></sup>.</p> 7 7 </blockquote> 8 8 <div class="footnote-definition" id="q"><sup class="footnote-definition-label">1</sup> 9 - <p>Footnote for quote.</p> 9 + <p dir="ltr">Footnote for quote.</p> 10 10 </div>
+3 -3
crates/weaver-renderer/src/static_site/snapshots/weaver_renderer__static_site__tests__footnote_multiple.snap
··· 2 2 source: crates/weaver-renderer/src/static_site/tests.rs 3 3 expression: output 4 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> 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 6 <div class="footnote-definition" id="1"><sup class="footnote-definition-label">1</sup> 7 - <p>First note.</p> 7 + <p dir="ltr">First note.</p> 8 8 </div> 9 9 <div class="footnote-definition" id="2"><sup class="footnote-definition-label">2</sup> 10 - <p>Second note.</p> 10 + <p dir="ltr">Second note.</p> 11 11 </div>
+1 -1
crates/weaver-renderer/src/static_site/snapshots/weaver_renderer__static_site__tests__footnote_named.snap
··· 2 2 source: crates/weaver-renderer/src/static_site/tests.rs 3 3 expression: output 4 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> 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 2 source: crates/weaver-renderer/src/static_site/tests.rs 3 3 expression: output 4 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> 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 2 source: crates/weaver-renderer/src/static_site/tests.rs 3 3 expression: output 4 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> 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 2 source: crates/weaver-renderer/src/static_site/tests.rs 3 3 expression: output 4 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> 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 2 source: crates/weaver-renderer/src/static_site/tests.rs 3 3 expression: output 4 4 --- 5 - <p>Inline <span class="math math-inline">x^2</span> and display:</p> 6 - <p><span class="math math-display"> 5 + <p dir="ltr">Inline <span class="math math-inline">x^2</span> and display:</p> 6 + <span class="math math-display"> 7 7 y = mx + b 8 - </span></p> 8 + </span><p></p>
+2 -2
crates/weaver-renderer/src/static_site/snapshots/weaver_renderer__static_site__tests__paragraph_rendering.snap
··· 2 2 source: crates/weaver-renderer/src/static_site/tests.rs 3 3 expression: output 4 4 --- 5 - <p>This is a paragraph.</p> 6 - <p>This is another paragraph.</p> 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 3 expression: output 4 4 --- 5 5 <aside> 6 - <p>This paragraph should be in an aside.</p> 6 + <p dir="ltr">This paragraph should be in an aside.</p> 7 7 </aside>
+1 -1
crates/weaver-renderer/src/static_site/snapshots/weaver_renderer__static_site__tests__weaver_block_before_blockquote.snap
··· 4 4 --- 5 5 <aside> 6 6 <blockquote> 7 - <p>This blockquote is in an aside.</p> 7 + <p dir="ltr">This blockquote is in an aside.</p> 8 8 </blockquote> 9 9 </aside>
+1 -1
crates/weaver-renderer/src/static_site/snapshots/weaver_renderer__static_site__tests__weaver_block_before_heading.snap
··· 4 4 --- 5 5 <aside> 6 6 <h2>Heading in aside</h2> 7 - <p>Paragraph also in aside.</p> 7 + <p dir="ltr">Paragraph also in aside.</p> 8 8 </aside>
+1 -1
crates/weaver-renderer/src/static_site/snapshots/weaver_renderer__static_site__tests__weaver_block_custom_attributes.snap
··· 3 3 expression: output 4 4 --- 5 5 <div class="foo" width="300px" data-test="value"> 6 - <p>Paragraph with class and attributes.</p> 6 + <p dir="ltr">Paragraph with class and attributes.</p> 7 7 </div>
+1 -1
crates/weaver-renderer/src/static_site/snapshots/weaver_renderer__static_site__tests__weaver_block_custom_class.snap
··· 3 3 expression: output 4 4 --- 5 5 <div class="highlight"> 6 - <p>This paragraph has a custom class.</p> 6 + <p dir="ltr">This paragraph has a custom class.</p> 7 7 </div>
+1 -1
crates/weaver-renderer/src/static_site/snapshots/weaver_renderer__static_site__tests__weaver_block_multiple_classes.snap
··· 3 3 expression: output 4 4 --- 5 5 <aside class="highlight important"> 6 - <p>Multiple classes applied.</p> 6 + <p dir="ltr">Multiple classes applied.</p> 7 7 </aside>
+2 -2
crates/weaver-renderer/src/static_site/snapshots/weaver_renderer__static_site__tests__weaver_block_no_effect_on_following.snap
··· 3 3 expression: output 4 4 --- 5 5 <aside> 6 - <p>First paragraph in aside.</p> 6 + <p dir="ltr">First paragraph in aside.</p> 7 7 </aside> 8 - <p>Second paragraph NOT in aside.</p> 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 3 expression: output 4 4 --- 5 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> 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 7 </aside>
+32 -4
crates/weaver-renderer/src/static_site/writer.rs
··· 48 48 in_sidenote: bool, 49 49 /// Whether we're deferring paragraph close for sidenote handling 50 50 defer_paragraph_close: bool, 51 + /// Buffered paragraph opening tag (without closing `>`) for dir attribute emission 52 + pending_paragraph_open: Option<String>, 51 53 } 52 54 53 55 impl<'input, I: Iterator<Item = Event<'input>>, A: AgentSession, W: StrWrite> ··· 72 74 pending_footnote_content: String::new(), 73 75 in_sidenote: false, 74 76 defer_paragraph_close: false, 77 + pending_paragraph_open: None, 75 78 } 76 79 } 77 80 ··· 130 133 /// Close deferred paragraph if we're in that state. 131 134 /// Called when a non-paragraph block element starts. 132 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 + } 133 141 if self.defer_paragraph_close { 134 142 // Flush pending footnote as traditional before closing 135 143 self.flush_pending_footnote()?; ··· 252 260 self.close_wrapper()?; 253 261 self.defer_paragraph_close = false; 254 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 + } 255 268 self.write("</p>\n")?; 256 269 self.block_depth -= 1; 257 270 self.close_wrapper()?; ··· 465 478 // Buffer text while waiting to see if footnote def follows 466 479 self.pending_footnote_content.push_str(&text); 467 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 + } 468 493 escape_html_body_text(&mut self.writer, &text)?; 469 494 self.end_newline = text.ends_with('\n'); 470 495 } ··· 601 626 self.flush_pending_footnote()?; 602 627 self.emit_wrapper_start()?; 603 628 self.block_depth += 1; 604 - if self.end_newline { 605 - self.write("<p>") 629 + // Buffer paragraph opening for dir attribute detection 630 + let opening = if self.end_newline { 631 + String::from("<p") 606 632 } else { 607 - self.write("\n<p>") 608 - } 633 + String::from("\n<p") 634 + }; 635 + self.pending_paragraph_open = Some(opening); 636 + Ok(()) 609 637 } 610 638 } 611 639 Tag::Heading {
+50 -1
crates/weaver-renderer/src/utils.rs
··· 1 - use markdown_weaver::{CodeBlockKind, CowStr, Event, Tag}; 1 + use markdown_weaver::CowStr; 2 2 use miette::IntoDiagnostic; 3 3 use n0_future::TryFutureExt; 4 4 use std::{path::Path, sync::OnceLock}; ··· 11 11 use markdown_weaver::BrokenLink; 12 12 use std::path::PathBuf; 13 13 use std::sync::Arc; 14 + use unicode_bidi::{get_base_direction, Direction}; 14 15 use unicode_normalization::UnicodeNormalization; 15 16 16 17 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))] ··· 207 208 } 208 209 } 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 22 23 23 {.aside} 24 24 > **On Platform Decay** 25 - > 26 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. 27 26 28 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.