svelte-streamdown 2.4.3 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,7 +32,7 @@ export const usePanzoom = (opts = {}) => {
32
32
  if (e.key === 'Escape' || e.keyCode === 27) {
33
33
  if (isExpanded && !animating) {
34
34
  // fire and forget
35
- void collapse();
35
+ void expand(false);
36
36
  }
37
37
  }
38
38
  }
@@ -345,59 +345,91 @@ export const usePanzoom = (opts = {}) => {
345
345
  };
346
346
  });
347
347
  }
348
- const collapse = () => {
348
+ const expand = (expand) => {
349
349
  if (!eventTarget)
350
350
  return;
351
- // Add view transition name for CSS targeting
352
- eventTarget.style.viewTransitionName = 'panzoom-element';
353
- isExpanded = false;
354
- const run = () => {
355
- if (!eventTarget)
356
- return;
357
- eventTarget.dataset.expanded = 'false';
351
+ // Capture first state
352
+ const first = eventTarget.getBoundingClientRect();
353
+ if (expand) {
354
+ // Expanding: immediately apply expanded state
355
+ eventTarget.parentElement.style.height = eventTarget.clientHeight + 'px';
356
+ eventTarget.dataset.expanded = 'true';
357
+ isExpanded = true;
358
358
  zoomToFit();
359
- };
360
- if (typeof document.startViewTransition === 'function') {
361
- document.startViewTransition(run).finished.finally(() => {
362
- if (eventTarget)
363
- eventTarget.style.viewTransitionName = '';
359
+ // Force layout to get the final position
360
+ const last = eventTarget.getBoundingClientRect();
361
+ // Calculate the inverse transform
362
+ const deltaX = first.left - last.left;
363
+ const deltaY = first.top - last.top;
364
+ const deltaW = first.width / last.width;
365
+ const deltaH = first.height / last.height;
366
+ // Animate from original position to expanded
367
+ const animation = eventTarget.animate([
368
+ {
369
+ transformOrigin: '0 0',
370
+ transform: `translate(${deltaX}px, ${deltaY}px) scale(${deltaW}, ${deltaH})`
371
+ },
372
+ {
373
+ transformOrigin: '0 0',
374
+ transform: 'translate(0px, 0px) scale(1, 1)'
375
+ }
376
+ ], {
377
+ duration: 350,
378
+ easing: 'cubic-bezier(0.4, 0.0, 0.2, 1)',
379
+ fill: 'both'
380
+ });
381
+ animation.finished.then(() => {
382
+ animation.cancel();
364
383
  });
365
384
  }
366
385
  else {
367
- run();
368
- if (eventTarget)
369
- eventTarget.style.viewTransitionName = '';
370
- }
371
- };
372
- const expand = () => {
373
- if (!eventTarget)
374
- return;
375
- // Add view transition name for CSS targeting
376
- eventTarget.style.viewTransitionName = 'panzoom-element';
377
- isExpanded = true;
378
- const run = () => {
379
- if (!eventTarget)
380
- return;
386
+ // Collapsing: keep expanded state during animation, then remove
387
+ const last = {
388
+ left: first.left,
389
+ top: first.top,
390
+ width: first.width,
391
+ height: first.height
392
+ };
393
+ // Calculate where it will be after collapsing
394
+ eventTarget.dataset.expanded = 'false';
395
+ const final = eventTarget.getBoundingClientRect();
396
+ // Restore expanded state for animation
381
397
  eventTarget.dataset.expanded = 'true';
382
- zoomToFit();
383
- };
384
- if (typeof document.startViewTransition === 'function') {
385
- document.startViewTransition(run).finished.finally(() => {
386
- if (eventTarget)
387
- eventTarget.style.viewTransitionName = '';
398
+ const deltaX = final.left - last.left;
399
+ const deltaY = final.top - last.top;
400
+ const deltaW = final.width / last.width;
401
+ const deltaH = final.height / last.height;
402
+ // Animate from expanded to collapsed
403
+ const animation = eventTarget.animate([
404
+ {
405
+ transformOrigin: '0 0',
406
+ transform: 'translate(0px, 0px) scale(1, 1)'
407
+ },
408
+ {
409
+ transformOrigin: '0 0',
410
+ transform: `translate(${deltaX}px, ${deltaY}px) scale(${deltaW}, ${deltaH})`
411
+ }
412
+ ], {
413
+ duration: 350,
414
+ easing: 'cubic-bezier(0.4, 0.0, 0.2, 1)',
415
+ fill: 'both'
416
+ });
417
+ animation.finished.then(() => {
418
+ animation.cancel();
419
+ // Only remove expanded state after animation completes
420
+ if (!eventTarget)
421
+ return;
422
+ eventTarget.dataset.expanded = 'false';
423
+ eventTarget.parentElement.style.height = 'fit-content';
424
+ isExpanded = false;
425
+ zoomToFit();
388
426
  });
389
- }
390
- else {
391
- run();
392
- if (eventTarget)
393
- eventTarget.style.viewTransitionName = '';
394
427
  }
395
428
  };
396
429
  async function toggleExpand() {
397
- zoomToFit();
398
430
  if (isExpanded)
399
- return collapse();
400
- return expand();
431
+ return expand(false);
432
+ return expand(true);
401
433
  }
402
434
  function zoomToFit(padding = 0.05) {
403
435
  if (!node)
@@ -467,7 +499,6 @@ export const usePanzoom = (opts = {}) => {
467
499
  moveBy,
468
500
  setTransform,
469
501
  expand,
470
- collapse,
471
502
  toggleExpand,
472
503
  get transform() {
473
504
  return { x, y, scale };
@@ -106,6 +106,8 @@ class IncompleteMarkdownParser {
106
106
  const lines = text.split('\n');
107
107
  let inCodeBlock = false;
108
108
  let inMathBlock = false;
109
+ let inCenterBlock = false;
110
+ let inRightBlock = false;
109
111
  // Track which lines are in which contexts for state management
110
112
  const lineContexts = [];
111
113
  for (let i = 0; i < lines.length; i++) {
@@ -117,7 +119,24 @@ class IncompleteMarkdownParser {
117
119
  if (line.trim().startsWith('$$') && !line.trim().includes('$$', 2)) {
118
120
  inMathBlock = !inMathBlock;
119
121
  }
120
- lineContexts[i] = { code: inCodeBlock, math: inMathBlock };
122
+ if (line.trim() === '[center]') {
123
+ inCenterBlock = true;
124
+ }
125
+ if (line.trim() === '[/center]') {
126
+ inCenterBlock = false;
127
+ }
128
+ if (line.trim() === '[right]') {
129
+ inRightBlock = true;
130
+ }
131
+ if (line.trim() === '[/right]') {
132
+ inRightBlock = false;
133
+ }
134
+ lineContexts[i] = {
135
+ code: inCodeBlock,
136
+ math: inMathBlock,
137
+ center: inCenterBlock,
138
+ right: inRightBlock
139
+ };
121
140
  }
122
141
  // Set the final blocking contexts (for postprocessing)
123
142
  const finalContexts = new Set();
@@ -125,6 +144,10 @@ class IncompleteMarkdownParser {
125
144
  finalContexts.add('code');
126
145
  if (inMathBlock)
127
146
  finalContexts.add('math');
147
+ if (inCenterBlock)
148
+ finalContexts.add('center');
149
+ if (inRightBlock)
150
+ finalContexts.add('right');
128
151
  // Return both the text and the updated state
129
152
  return {
130
153
  text: text, // Don't modify text in preprocess
@@ -142,6 +165,12 @@ class IncompleteMarkdownParser {
142
165
  if (state.blockingContexts.has('math')) {
143
166
  return text + '\n$$';
144
167
  }
168
+ if (state.blockingContexts.has('center')) {
169
+ return text + '\n[/center]';
170
+ }
171
+ if (state.blockingContexts.has('right')) {
172
+ return text + '\n[/right]';
173
+ }
145
174
  return text;
146
175
  }
147
176
  },
@@ -153,10 +182,14 @@ class IncompleteMarkdownParser {
153
182
  if (line.trim() === '***') {
154
183
  return line;
155
184
  }
185
+ const isEndingWithTripleAsterisk = line.endsWith('***');
156
186
  const tripleAsterisks = (line.match(/\*\*\*/g) || []).length;
157
187
  if (tripleAsterisks % 2 === 1) {
158
188
  const lastTripleAsteriskIndex = line.lastIndexOf('***');
159
189
  const endOfCellOrLine = findEndOfCellOrLineContaining(line, lastTripleAsteriskIndex);
190
+ if (isEndingWithTripleAsterisk) {
191
+ return line.substring(0, lastTripleAsteriskIndex);
192
+ }
160
193
  return line.substring(0, endOfCellOrLine) + '***' + line.substring(endOfCellOrLine);
161
194
  }
162
195
  return line;
@@ -172,8 +205,12 @@ class IncompleteMarkdownParser {
172
205
  }
173
206
  const doubleAsteriskMatches = (line.match(/\*\*/g) || []).length;
174
207
  if (doubleAsteriskMatches % 2 === 1) {
208
+ const isEndingWithDoubleAsterisk = line.endsWith('**');
175
209
  const lastDoubleAsteriskIndex = line.lastIndexOf('**');
176
210
  const endOfCellOrLine = findEndOfCellOrLineContaining(line, lastDoubleAsteriskIndex);
211
+ if (isEndingWithDoubleAsterisk) {
212
+ return line.substring(0, lastDoubleAsteriskIndex);
213
+ }
177
214
  return line.substring(0, endOfCellOrLine) + '**' + line.substring(endOfCellOrLine);
178
215
  }
179
216
  return line;
@@ -189,8 +226,12 @@ class IncompleteMarkdownParser {
189
226
  }
190
227
  const underscorePairs = (line.match(/__/g) || []).length;
191
228
  if (underscorePairs % 2 === 1) {
229
+ const isEndingWithDoubleUnderscore = line.endsWith('__');
192
230
  const lastDoubleUnderscoreIndex = line.lastIndexOf('__');
193
231
  const endOfCellOrLine = findEndOfCellOrLineContaining(line, lastDoubleUnderscoreIndex);
232
+ if (isEndingWithDoubleUnderscore) {
233
+ return line.substring(0, lastDoubleUnderscoreIndex);
234
+ }
194
235
  return line.substring(0, endOfCellOrLine) + '__' + line.substring(endOfCellOrLine);
195
236
  }
196
237
  return line;
@@ -203,11 +244,15 @@ class IncompleteMarkdownParser {
203
244
  handler: ({ line }) => {
204
245
  const tildePairs = (line.match(/~~/g) || []).length;
205
246
  if (tildePairs % 2 === 1) {
247
+ const isEndingWithDoubleTilde = line.endsWith('~~');
206
248
  const lastDoubleTildeIndex = line.lastIndexOf('~~');
207
249
  const endOfCellOrLine = findEndOfCellOrLineContaining(line, lastDoubleTildeIndex);
208
250
  // Only complete if there's content after the tildes
209
251
  const contentAfterTildes = line.substring(lastDoubleTildeIndex + 2, endOfCellOrLine);
210
252
  if (contentAfterTildes.trim().length > 0) {
253
+ if (isEndingWithDoubleTilde) {
254
+ return line.substring(0, lastDoubleTildeIndex);
255
+ }
211
256
  return line.substring(0, endOfCellOrLine) + '~~' + line.substring(endOfCellOrLine);
212
257
  }
213
258
  }
@@ -390,6 +435,31 @@ class IncompleteMarkdownParser {
390
435
  return line;
391
436
  }
392
437
  },
438
+ {
439
+ name: 'inlineCitation',
440
+ pattern: /\[/,
441
+ skipInBlockTypes: ['code', 'math'],
442
+ handler: ({ line }) => {
443
+ // Count unescaped opening brackets without matching closing brackets
444
+ let unclosedBrackets = 0;
445
+ for (let i = 0; i < line.length; i++) {
446
+ if (line[i] === '[' && (i === 0 || line[i - 1] !== '\\')) {
447
+ // Check if this bracket has a matching closing bracket later in the line
448
+ const restOfLine = line.substring(i + 1);
449
+ const closingIndex = restOfLine.indexOf(']');
450
+ if (closingIndex === -1) {
451
+ unclosedBrackets++;
452
+ }
453
+ }
454
+ }
455
+ // If there's an odd number of unclosed brackets, add closing bracket
456
+ if (unclosedBrackets % 2 === 1) {
457
+ const endOfCellOrLine = findEndOfCellOrLineContaining(line, line.length - 1);
458
+ return line.substring(0, endOfCellOrLine) + ']' + line.substring(endOfCellOrLine);
459
+ }
460
+ return line;
461
+ }
462
+ },
393
463
  {
394
464
  name: 'footnoteRef',
395
465
  pattern: /\[\^[^\]\s,]*/,
@@ -482,6 +552,9 @@ class IncompleteMarkdownParser {
482
552
  pattern: /\$\$/,
483
553
  skipInBlockTypes: ['code', 'math'],
484
554
  handler: ({ line }) => {
555
+ // Don't process block boundaries (lines that are just $$)
556
+ if (line.trim() === '$$')
557
+ return line;
485
558
  const dollarPairs = (line.match(/\$\$/g) || []).length;
486
559
  if (dollarPairs % 2 === 0)
487
560
  return line;
@@ -582,6 +655,21 @@ class IncompleteMarkdownParser {
582
655
  }
583
656
  return line;
584
657
  }
658
+ },
659
+ {
660
+ name: 'alignmentBlocks',
661
+ pattern: /^(\s*\[(center|right)\])$/,
662
+ skipInBlockTypes: ['code', 'math'],
663
+ handler: ({ line, state }) => {
664
+ // Check if this is an opening alignment tag without content or closing tag
665
+ const alignMatch = line.match(/^(\s*\[(center|right)\])$/);
666
+ if (alignMatch) {
667
+ const indent = alignMatch[1].length - alignMatch[1].trim().length;
668
+ const alignType = alignMatch[2];
669
+ return line + '\n' + ' '.repeat(indent) + '[/' + alignType + ']';
670
+ }
671
+ return line;
672
+ }
585
673
  }
586
674
  ];
587
675
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-streamdown",
3
- "version": "2.4.3",
3
+ "version": "2.5.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",