@sofereditor/core 0.3.1 → 0.4.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.
package/dist/index.d.cts CHANGED
@@ -91,6 +91,13 @@ type ListKind = "bullet" | "ordered";
91
91
  type ListStyleType = "decimal" | "lower-alpha" | "upper-alpha" | "lower-roman" | "upper-roman";
92
92
  /** Maximum supported list nesting (0 = top level). */
93
93
  declare const MAX_LIST_LEVEL = 5;
94
+ /** Entrelinha de uma linha de resposta: simples, 1,5 ou dupla. */
95
+ type AnswerLineSpacing = 1 | 1.5 | 2;
96
+ /**
97
+ * Onde as linhas da grade de uma tabela aparecem.
98
+ * Vocabulário nativo de `w:tblBorders` (top/left/bottom/right/insideH/insideV).
99
+ */
100
+ type TableBorderPreset = "all" | "outer" | "horizontal" | "vertical" | "none";
94
101
  interface BlockAttrs {
95
102
  align?: AlignValue;
96
103
  dir?: DirValue;
@@ -126,6 +133,24 @@ interface BlockAttrs {
126
133
  * an auto distribution at render time.
127
134
  */
128
135
  colWidths?: number[];
136
+ /**
137
+ * Só relevante quando `type === "table"`. Onde as linhas da grade aparecem.
138
+ * Ausente = "all" — documentos existentes não mudam de aparência.
139
+ */
140
+ borderPreset?: TableBorderPreset;
141
+ /**
142
+ * Só relevante quando `type === "paragraph"`. Parágrafo pautado para o aluno
143
+ * escrever a resposta: renderiza com régua inferior de largura total.
144
+ */
145
+ answerLine?: true;
146
+ /**
147
+ * Só relevante quando `answerLine`. Entrelinha. Ausente = 1.
148
+ *
149
+ * Deliberadamente escopado a linhas de resposta em vez de um `lineSpacing`
150
+ * genérico de parágrafo: entrelinha geral mudaria a medição de paginação de
151
+ * TODO parágrafo do documento, o que é outra feature com outro raio de impacto.
152
+ */
153
+ answerLineSpacing?: AnswerLineSpacing;
129
154
  }
130
155
  interface CellAttrs {
131
156
  /**
@@ -146,6 +171,8 @@ interface CellAttrs {
146
171
  covered?: true;
147
172
  /** Alinhamento horizontal do texto da célula. Mesmos valores de bloco. */
148
173
  align?: AlignValue;
174
+ /** Cor de fundo da célula (CSS color, ex. `#ffe58f`). Ausente = sem fundo. */
175
+ bgColor?: string;
149
176
  }
150
177
  interface Position {
151
178
  blockIndex: number;
@@ -163,7 +190,7 @@ interface Selection {
163
190
  anchor: Position;
164
191
  focus: Position;
165
192
  }
166
- type MarkName = "bold" | "italic" | "underline" | "strike" | "color" | "fontFamily" | "fontSize" | "link" | "comment";
193
+ type MarkName = "bold" | "italic" | "underline" | "strike" | "color" | "highlight" | "fontFamily" | "fontSize" | "link" | "comment";
167
194
  interface LinkAttr {
168
195
  href: string;
169
196
  title?: string;
@@ -178,6 +205,8 @@ interface MarkAttrs {
178
205
  underline?: true;
179
206
  strike?: true;
180
207
  color?: string;
208
+ /** Cor de fundo do texto (marca-texto). CSS color, ex. `#fff176`. */
209
+ highlight?: string;
181
210
  fontFamily?: string;
182
211
  fontSize?: string;
183
212
  link?: LinkAttr;
@@ -448,6 +477,115 @@ declare function deltaLength(delta: DeltaOp[]): number;
448
477
  */
449
478
  declare function sliceDelta(delta: DeltaOp[], start: number, end: number): DeltaOp[];
450
479
 
480
+ /**
481
+ * Helpers puros de decoração visual, compartilhados pelos DOIS renderizadores
482
+ * (`@sofereditor/react` e `@sofereditor/export-pdf`).
483
+ *
484
+ * Ficam aqui, e não dentro de cada renderizador, porque decoração divergente
485
+ * entre editor e PDF é exatamente o modo de falha que a fidelidade de impressão
486
+ * do projeto existe para impedir.
487
+ */
488
+
489
+ /** Estilo CSS em camelCase — compatível com `CSSProperties` do React. */
490
+ type StyleRecord = Record<string, string>;
491
+ /** Mínimo de underlines consecutivos que viram lacuna. */
492
+ declare const BLANK_MIN_RUN = 3;
493
+ /**
494
+ * Segmenta um texto em trechos normais e corridas de `BLANK_MIN_RUN`+ underlines.
495
+ *
496
+ * INVARIANTE: a concatenação dos segmentos reconstrói o texto original
497
+ * caractere a caractere. Os offsets do modelo dependem disso — `dom-bridge`
498
+ * mapeia posição de DOM para offset somando `textContent.length`, então dividir
499
+ * uma run em sub-spans é seguro, mas introduzir ou remover caracteres não é.
500
+ */
501
+ declare function splitUnderscoreRuns(text: string): Array<{
502
+ text: string;
503
+ blank: boolean;
504
+ }>;
505
+ /**
506
+ * Estilo da lacuna: uma corrida de underlines vira um traço contínuo, sem
507
+ * serrilhado, com a largura exata da sequência digitada.
508
+ *
509
+ * `-webkit-text-fill-color` apaga o GLIFO mas preserva `color`.
510
+ *
511
+ * O traço é `border-bottom`, e NÃO `text-decoration: underline`: no Chrome o
512
+ * `-webkit-text-fill-color` também apaga a decoração de texto — inclusive com
513
+ * `text-decoration-color: currentColor`, que resolve para a cor de
514
+ * PREENCHIMENTO, não para `color`. Verificado no navegador: a variante com
515
+ * `underline` renderiza uma lacuna completamente invisível. `border-bottom`
516
+ * com `currentColor` resolve para `color` e acompanha a mark de cor do run.
517
+ *
518
+ * PROIBIDO acrescentar aqui: `display`, `padding`, `margin`, `letterSpacing`,
519
+ * `width`. Qualquer um desloca métricas e a paginação diverge do PDF. Bordas de
520
+ * elemento inline não afetam a altura da linha — medido: linha com e sem
521
+ * lacuna tem exatamente a mesma altura e a mesma largura.
522
+ */
523
+ declare const BLANK_STYLE: StyleRecord;
524
+ /** Cor da régua da linha de resposta. Preto: é uma linha para escrever à caneta. */
525
+ declare const ANSWER_LINE_COLOR = "#000000";
526
+ /**
527
+ * Estilo do parágrafo pautado. `undefined` quando o bloco não é linha de
528
+ * resposta, para o renderizador não emitir atributo `style` à toa.
529
+ *
530
+ * Diferente das bordas de tabela (cuja geometria é invariante por construção),
531
+ * `lineHeight` muda a altura medida do bloco DE PROPÓSITO — a paginação precisa
532
+ * medir o resultado, não presumir a entrelinha.
533
+ */
534
+ declare function answerLineStyle(attrs: BlockAttrs): StyleRecord | undefined;
535
+ /** Converte um `StyleRecord` camelCase em texto CSS (`a:b;c:d`). */
536
+ declare function styleToCssText(style: StyleRecord): string;
537
+
538
+ /** Cor da grade. Mesmo valor que o CSS de `.ed-cell` já usa. */
539
+ declare const TABLE_BORDER_COLOR = "#cbd5e1";
540
+ /**
541
+ * Guia de tela para lados de borda desligados por preset.
542
+ *
543
+ * É uma custom property com fallback `transparent`: sem a variável definida
544
+ * (consumidor sem CSS próprio, ou dentro de `@media print`), o lado
545
+ * simplesmente não aparece — que é o correto para impressão. Nunca muda a
546
+ * geometria: o lado continua com 1px, só sem cor.
547
+ */
548
+ declare const TABLE_GUIDE_COLOR = "var(--ed-guide-color, transparent)";
549
+ interface CellBorderPos {
550
+ /** Linha absoluta da célula na tabela lógica. */
551
+ row: number;
552
+ /** Coluna absoluta. */
553
+ col: number;
554
+ rowspan: number;
555
+ colspan: number;
556
+ /** Total de colunas da tabela. */
557
+ cols: number;
558
+ /** Primeira linha do fragmento renderizado. Tabela inteira = 0. */
559
+ rowStart: number;
560
+ /** Fim exclusivo do fragmento. Tabela inteira = `rows`. */
561
+ rowEnd: number;
562
+ }
563
+ interface CellBorderColors {
564
+ top: string;
565
+ right: string;
566
+ bottom: string;
567
+ left: string;
568
+ }
569
+ /**
570
+ * Cor de cada lado da célula segundo o preset.
571
+ *
572
+ * NUNCA devolve espessura. Trocar de preset muda só cor, então nenhuma linha
573
+ * reflui e a paginação já validada não se mexe. Como todas as bordas têm a
574
+ * mesma espessura e o mesmo estilo em toda a tabela, também não há conflito de
575
+ * `border-collapse` para resolver.
576
+ *
577
+ * As bordas externas seguem os limites do FRAGMENTO, não da tabela lógica: numa
578
+ * tabela quebrada entre páginas, cada página fecha a própria caixa — o
579
+ * comportamento do Word.
580
+ *
581
+ * O teste de "toca a borda" usa row/col + spans em vez dos seletores CSS
582
+ * `:first-child`/`:last-child`, que quebram assim que uma célula `covered` some
583
+ * do DOM por causa de um rowspan.
584
+ */
585
+ declare function cellBorderColors(preset: TableBorderPreset | undefined, pos: CellBorderPos, variant: "screen" | "print"): CellBorderColors;
586
+ /** `cellBorderColors` no formato de estilo inline consumido pelos renderizadores. */
587
+ declare function cellBorderStyle(preset: TableBorderPreset | undefined, pos: CellBorderPos, variant: "screen" | "print"): StyleRecord;
588
+
451
589
  /** MIME custom que carrega o slice serializado no clipboard. */
452
590
  declare const SOFER_MIME = "application/x-sofer-slice";
453
591
  interface ClipboardSlice {
@@ -513,6 +651,14 @@ declare function setBlockType(ctx: CommandContext, type: BlockType, attrs?: Bloc
513
651
  * Passing `value === null` deletes the key.
514
652
  */
515
653
  declare function setBlockAttr<K extends keyof BlockAttrs>(ctx: CommandContext, key: K, value: BlockAttrs[K] | null): void;
654
+ /**
655
+ * Como `setBlockAttr`, mas em um bloco específico por índice.
656
+ *
657
+ * Existe porque `setBlockAttr` opera sobre a seleção e é INERTE quando o caret
658
+ * está dentro de uma célula — que é exatamente a situação em que se quer mudar
659
+ * um atributo da tabela (ex.: preset de borda).
660
+ */
661
+ declare function setBlockAttrAtIndex<K extends keyof BlockAttrs>(ctx: CommandContext, blockIndex: number, key: K, value: BlockAttrs[K] | null): void;
516
662
  /**
517
663
  * Set a single attribute on the table cell(s) under the selection.
518
664
  * - Caret numa única célula → essa célula (tableRectSelection retorna null
@@ -565,6 +711,16 @@ declare function splitListItem(ctx: CommandContext): boolean;
565
711
  * preceding paragraph. Callers can clean that up themselves; an extra empty
566
712
  * paragraph above a table is a small price for predictable behavior.
567
713
  */
714
+ /** Teto de linhas por inserção — protege contra digitar 5000 no popover. */
715
+ declare const ANSWER_LINE_MAX = 50;
716
+ /**
717
+ * Insere `count` parágrafos pautados depois do bloco focado, numa única
718
+ * transação (um passo de undo desfaz todos).
719
+ *
720
+ * Se o caret estiver dentro de uma célula, `focus.blockIndex` já aponta para a
721
+ * TABELA, então as linhas entram depois dela — nunca dentro.
722
+ */
723
+ declare function insertAnswerLines(ctx: CommandContext, count: number, spacing: AnswerLineSpacing): void;
568
724
  declare function insertTable(ctx: CommandContext, rows: number, cols: number): void;
569
725
  /** Remove the entire table at `blockIndex` and place the caret at the start of the next block (or the previous one if removal would leave the doc empty). */
570
726
  declare function deleteTable(ctx: CommandContext, blockIndex: number): void;
@@ -730,4 +886,4 @@ declare class EditorHistory {
730
886
  destroy(): void;
731
887
  }
732
888
 
733
- export { type AlignValue, BLOCK_SPECS, type BlockAttrs, type BlockType, CLEAR_ALL_MARKS, COMMAND_ORIGIN, type CellAttrs, type ClipboardSlice, type CommandContext, type CommentAttr, DEFAULT_PAGE_SETTINGS, type DeltaOp, type DirValue, EditorDocument, EditorHistory, type EmbedLoc, type EncodedCursor, type EncodedCursorPoint, type ImageEmbed, type ImageLayout, type InsertContent, type LegacySerializedDocument, type LinkAttr, type ListKind, type ListStyleType, MARGIN_PRESETS, MAX_LIST_LEVEL, type MarkAttrs, type MarkName, type MarkValue, PAGE_PRESETS, type PagePreset, type PageSettings, type Position, SOFER_MIME, type Selection, type SerializedBlock, type SerializedCell, type SerializedDocument, TRACKED_ORIGINS, type TableLocation, type TableRect, cellsInRect, clearRectCells, collapsedSelection, comparePositions, createBlock, createCell, createTableBlock, decodeSelection, dedentList, defaultAttrsFor, deleteBackward, deleteForward, deleteSelection, deleteTable, deleteTableColumn, deleteTableRow, deltaLength, detectPreset, encodeSelection, expandTableRect, getBlockId, getMarksInRange, indentList, insertImage, insertParagraph, insertSlice, insertTable, insertTableColumn, insertTableRow, insertText, isCollapsed, isCommandOrigin, isImageEmbed, isKnownBlockType, isLegacySerializedDocument, isMarkUniformInRange, mergeDown, mergeRight, mergeSelection, mmToPx, moveEmbedAnchor, moveToNextCell, moveToPrevCell, newBlockId, orderedRange, positionsEqual, pxToMm, removeMark, sameTextRun, serializeSelection, setBlockAttr, setBlockType, setCellAttr, setColumnWidth, setImageAttrs, setMark, sliceDelta, sliceToInlineDelta, sliceToText, spanOf, splitCell, splitListItem, tableLocationOf, tableRectSelection, toggleList, toggleMark, withDefaults };
889
+ export { ANSWER_LINE_COLOR, ANSWER_LINE_MAX, type AlignValue, type AnswerLineSpacing, BLANK_MIN_RUN, BLANK_STYLE, BLOCK_SPECS, type BlockAttrs, type BlockType, CLEAR_ALL_MARKS, COMMAND_ORIGIN, type CellAttrs, type CellBorderColors, type CellBorderPos, type ClipboardSlice, type CommandContext, type CommentAttr, DEFAULT_PAGE_SETTINGS, type DeltaOp, type DirValue, EditorDocument, EditorHistory, type EmbedLoc, type EncodedCursor, type EncodedCursorPoint, type ImageEmbed, type ImageLayout, type InsertContent, type LegacySerializedDocument, type LinkAttr, type ListKind, type ListStyleType, MARGIN_PRESETS, MAX_LIST_LEVEL, type MarkAttrs, type MarkName, type MarkValue, PAGE_PRESETS, type PagePreset, type PageSettings, type Position, SOFER_MIME, type Selection, type SerializedBlock, type SerializedCell, type SerializedDocument, type StyleRecord, TABLE_BORDER_COLOR, TABLE_GUIDE_COLOR, TRACKED_ORIGINS, type TableBorderPreset, type TableLocation, type TableRect, answerLineStyle, cellBorderColors, cellBorderStyle, cellsInRect, clearRectCells, collapsedSelection, comparePositions, createBlock, createCell, createTableBlock, decodeSelection, dedentList, defaultAttrsFor, deleteBackward, deleteForward, deleteSelection, deleteTable, deleteTableColumn, deleteTableRow, deltaLength, detectPreset, encodeSelection, expandTableRect, getBlockId, getMarksInRange, indentList, insertAnswerLines, insertImage, insertParagraph, insertSlice, insertTable, insertTableColumn, insertTableRow, insertText, isCollapsed, isCommandOrigin, isImageEmbed, isKnownBlockType, isLegacySerializedDocument, isMarkUniformInRange, mergeDown, mergeRight, mergeSelection, mmToPx, moveEmbedAnchor, moveToNextCell, moveToPrevCell, newBlockId, orderedRange, positionsEqual, pxToMm, removeMark, sameTextRun, serializeSelection, setBlockAttr, setBlockAttrAtIndex, setBlockType, setCellAttr, setColumnWidth, setImageAttrs, setMark, sliceDelta, sliceToInlineDelta, sliceToText, spanOf, splitCell, splitListItem, splitUnderscoreRuns, styleToCssText, tableLocationOf, tableRectSelection, toggleList, toggleMark, withDefaults };
package/dist/index.d.ts CHANGED
@@ -91,6 +91,13 @@ type ListKind = "bullet" | "ordered";
91
91
  type ListStyleType = "decimal" | "lower-alpha" | "upper-alpha" | "lower-roman" | "upper-roman";
92
92
  /** Maximum supported list nesting (0 = top level). */
93
93
  declare const MAX_LIST_LEVEL = 5;
94
+ /** Entrelinha de uma linha de resposta: simples, 1,5 ou dupla. */
95
+ type AnswerLineSpacing = 1 | 1.5 | 2;
96
+ /**
97
+ * Onde as linhas da grade de uma tabela aparecem.
98
+ * Vocabulário nativo de `w:tblBorders` (top/left/bottom/right/insideH/insideV).
99
+ */
100
+ type TableBorderPreset = "all" | "outer" | "horizontal" | "vertical" | "none";
94
101
  interface BlockAttrs {
95
102
  align?: AlignValue;
96
103
  dir?: DirValue;
@@ -126,6 +133,24 @@ interface BlockAttrs {
126
133
  * an auto distribution at render time.
127
134
  */
128
135
  colWidths?: number[];
136
+ /**
137
+ * Só relevante quando `type === "table"`. Onde as linhas da grade aparecem.
138
+ * Ausente = "all" — documentos existentes não mudam de aparência.
139
+ */
140
+ borderPreset?: TableBorderPreset;
141
+ /**
142
+ * Só relevante quando `type === "paragraph"`. Parágrafo pautado para o aluno
143
+ * escrever a resposta: renderiza com régua inferior de largura total.
144
+ */
145
+ answerLine?: true;
146
+ /**
147
+ * Só relevante quando `answerLine`. Entrelinha. Ausente = 1.
148
+ *
149
+ * Deliberadamente escopado a linhas de resposta em vez de um `lineSpacing`
150
+ * genérico de parágrafo: entrelinha geral mudaria a medição de paginação de
151
+ * TODO parágrafo do documento, o que é outra feature com outro raio de impacto.
152
+ */
153
+ answerLineSpacing?: AnswerLineSpacing;
129
154
  }
130
155
  interface CellAttrs {
131
156
  /**
@@ -146,6 +171,8 @@ interface CellAttrs {
146
171
  covered?: true;
147
172
  /** Alinhamento horizontal do texto da célula. Mesmos valores de bloco. */
148
173
  align?: AlignValue;
174
+ /** Cor de fundo da célula (CSS color, ex. `#ffe58f`). Ausente = sem fundo. */
175
+ bgColor?: string;
149
176
  }
150
177
  interface Position {
151
178
  blockIndex: number;
@@ -163,7 +190,7 @@ interface Selection {
163
190
  anchor: Position;
164
191
  focus: Position;
165
192
  }
166
- type MarkName = "bold" | "italic" | "underline" | "strike" | "color" | "fontFamily" | "fontSize" | "link" | "comment";
193
+ type MarkName = "bold" | "italic" | "underline" | "strike" | "color" | "highlight" | "fontFamily" | "fontSize" | "link" | "comment";
167
194
  interface LinkAttr {
168
195
  href: string;
169
196
  title?: string;
@@ -178,6 +205,8 @@ interface MarkAttrs {
178
205
  underline?: true;
179
206
  strike?: true;
180
207
  color?: string;
208
+ /** Cor de fundo do texto (marca-texto). CSS color, ex. `#fff176`. */
209
+ highlight?: string;
181
210
  fontFamily?: string;
182
211
  fontSize?: string;
183
212
  link?: LinkAttr;
@@ -448,6 +477,115 @@ declare function deltaLength(delta: DeltaOp[]): number;
448
477
  */
449
478
  declare function sliceDelta(delta: DeltaOp[], start: number, end: number): DeltaOp[];
450
479
 
480
+ /**
481
+ * Helpers puros de decoração visual, compartilhados pelos DOIS renderizadores
482
+ * (`@sofereditor/react` e `@sofereditor/export-pdf`).
483
+ *
484
+ * Ficam aqui, e não dentro de cada renderizador, porque decoração divergente
485
+ * entre editor e PDF é exatamente o modo de falha que a fidelidade de impressão
486
+ * do projeto existe para impedir.
487
+ */
488
+
489
+ /** Estilo CSS em camelCase — compatível com `CSSProperties` do React. */
490
+ type StyleRecord = Record<string, string>;
491
+ /** Mínimo de underlines consecutivos que viram lacuna. */
492
+ declare const BLANK_MIN_RUN = 3;
493
+ /**
494
+ * Segmenta um texto em trechos normais e corridas de `BLANK_MIN_RUN`+ underlines.
495
+ *
496
+ * INVARIANTE: a concatenação dos segmentos reconstrói o texto original
497
+ * caractere a caractere. Os offsets do modelo dependem disso — `dom-bridge`
498
+ * mapeia posição de DOM para offset somando `textContent.length`, então dividir
499
+ * uma run em sub-spans é seguro, mas introduzir ou remover caracteres não é.
500
+ */
501
+ declare function splitUnderscoreRuns(text: string): Array<{
502
+ text: string;
503
+ blank: boolean;
504
+ }>;
505
+ /**
506
+ * Estilo da lacuna: uma corrida de underlines vira um traço contínuo, sem
507
+ * serrilhado, com a largura exata da sequência digitada.
508
+ *
509
+ * `-webkit-text-fill-color` apaga o GLIFO mas preserva `color`.
510
+ *
511
+ * O traço é `border-bottom`, e NÃO `text-decoration: underline`: no Chrome o
512
+ * `-webkit-text-fill-color` também apaga a decoração de texto — inclusive com
513
+ * `text-decoration-color: currentColor`, que resolve para a cor de
514
+ * PREENCHIMENTO, não para `color`. Verificado no navegador: a variante com
515
+ * `underline` renderiza uma lacuna completamente invisível. `border-bottom`
516
+ * com `currentColor` resolve para `color` e acompanha a mark de cor do run.
517
+ *
518
+ * PROIBIDO acrescentar aqui: `display`, `padding`, `margin`, `letterSpacing`,
519
+ * `width`. Qualquer um desloca métricas e a paginação diverge do PDF. Bordas de
520
+ * elemento inline não afetam a altura da linha — medido: linha com e sem
521
+ * lacuna tem exatamente a mesma altura e a mesma largura.
522
+ */
523
+ declare const BLANK_STYLE: StyleRecord;
524
+ /** Cor da régua da linha de resposta. Preto: é uma linha para escrever à caneta. */
525
+ declare const ANSWER_LINE_COLOR = "#000000";
526
+ /**
527
+ * Estilo do parágrafo pautado. `undefined` quando o bloco não é linha de
528
+ * resposta, para o renderizador não emitir atributo `style` à toa.
529
+ *
530
+ * Diferente das bordas de tabela (cuja geometria é invariante por construção),
531
+ * `lineHeight` muda a altura medida do bloco DE PROPÓSITO — a paginação precisa
532
+ * medir o resultado, não presumir a entrelinha.
533
+ */
534
+ declare function answerLineStyle(attrs: BlockAttrs): StyleRecord | undefined;
535
+ /** Converte um `StyleRecord` camelCase em texto CSS (`a:b;c:d`). */
536
+ declare function styleToCssText(style: StyleRecord): string;
537
+
538
+ /** Cor da grade. Mesmo valor que o CSS de `.ed-cell` já usa. */
539
+ declare const TABLE_BORDER_COLOR = "#cbd5e1";
540
+ /**
541
+ * Guia de tela para lados de borda desligados por preset.
542
+ *
543
+ * É uma custom property com fallback `transparent`: sem a variável definida
544
+ * (consumidor sem CSS próprio, ou dentro de `@media print`), o lado
545
+ * simplesmente não aparece — que é o correto para impressão. Nunca muda a
546
+ * geometria: o lado continua com 1px, só sem cor.
547
+ */
548
+ declare const TABLE_GUIDE_COLOR = "var(--ed-guide-color, transparent)";
549
+ interface CellBorderPos {
550
+ /** Linha absoluta da célula na tabela lógica. */
551
+ row: number;
552
+ /** Coluna absoluta. */
553
+ col: number;
554
+ rowspan: number;
555
+ colspan: number;
556
+ /** Total de colunas da tabela. */
557
+ cols: number;
558
+ /** Primeira linha do fragmento renderizado. Tabela inteira = 0. */
559
+ rowStart: number;
560
+ /** Fim exclusivo do fragmento. Tabela inteira = `rows`. */
561
+ rowEnd: number;
562
+ }
563
+ interface CellBorderColors {
564
+ top: string;
565
+ right: string;
566
+ bottom: string;
567
+ left: string;
568
+ }
569
+ /**
570
+ * Cor de cada lado da célula segundo o preset.
571
+ *
572
+ * NUNCA devolve espessura. Trocar de preset muda só cor, então nenhuma linha
573
+ * reflui e a paginação já validada não se mexe. Como todas as bordas têm a
574
+ * mesma espessura e o mesmo estilo em toda a tabela, também não há conflito de
575
+ * `border-collapse` para resolver.
576
+ *
577
+ * As bordas externas seguem os limites do FRAGMENTO, não da tabela lógica: numa
578
+ * tabela quebrada entre páginas, cada página fecha a própria caixa — o
579
+ * comportamento do Word.
580
+ *
581
+ * O teste de "toca a borda" usa row/col + spans em vez dos seletores CSS
582
+ * `:first-child`/`:last-child`, que quebram assim que uma célula `covered` some
583
+ * do DOM por causa de um rowspan.
584
+ */
585
+ declare function cellBorderColors(preset: TableBorderPreset | undefined, pos: CellBorderPos, variant: "screen" | "print"): CellBorderColors;
586
+ /** `cellBorderColors` no formato de estilo inline consumido pelos renderizadores. */
587
+ declare function cellBorderStyle(preset: TableBorderPreset | undefined, pos: CellBorderPos, variant: "screen" | "print"): StyleRecord;
588
+
451
589
  /** MIME custom que carrega o slice serializado no clipboard. */
452
590
  declare const SOFER_MIME = "application/x-sofer-slice";
453
591
  interface ClipboardSlice {
@@ -513,6 +651,14 @@ declare function setBlockType(ctx: CommandContext, type: BlockType, attrs?: Bloc
513
651
  * Passing `value === null` deletes the key.
514
652
  */
515
653
  declare function setBlockAttr<K extends keyof BlockAttrs>(ctx: CommandContext, key: K, value: BlockAttrs[K] | null): void;
654
+ /**
655
+ * Como `setBlockAttr`, mas em um bloco específico por índice.
656
+ *
657
+ * Existe porque `setBlockAttr` opera sobre a seleção e é INERTE quando o caret
658
+ * está dentro de uma célula — que é exatamente a situação em que se quer mudar
659
+ * um atributo da tabela (ex.: preset de borda).
660
+ */
661
+ declare function setBlockAttrAtIndex<K extends keyof BlockAttrs>(ctx: CommandContext, blockIndex: number, key: K, value: BlockAttrs[K] | null): void;
516
662
  /**
517
663
  * Set a single attribute on the table cell(s) under the selection.
518
664
  * - Caret numa única célula → essa célula (tableRectSelection retorna null
@@ -565,6 +711,16 @@ declare function splitListItem(ctx: CommandContext): boolean;
565
711
  * preceding paragraph. Callers can clean that up themselves; an extra empty
566
712
  * paragraph above a table is a small price for predictable behavior.
567
713
  */
714
+ /** Teto de linhas por inserção — protege contra digitar 5000 no popover. */
715
+ declare const ANSWER_LINE_MAX = 50;
716
+ /**
717
+ * Insere `count` parágrafos pautados depois do bloco focado, numa única
718
+ * transação (um passo de undo desfaz todos).
719
+ *
720
+ * Se o caret estiver dentro de uma célula, `focus.blockIndex` já aponta para a
721
+ * TABELA, então as linhas entram depois dela — nunca dentro.
722
+ */
723
+ declare function insertAnswerLines(ctx: CommandContext, count: number, spacing: AnswerLineSpacing): void;
568
724
  declare function insertTable(ctx: CommandContext, rows: number, cols: number): void;
569
725
  /** Remove the entire table at `blockIndex` and place the caret at the start of the next block (or the previous one if removal would leave the doc empty). */
570
726
  declare function deleteTable(ctx: CommandContext, blockIndex: number): void;
@@ -730,4 +886,4 @@ declare class EditorHistory {
730
886
  destroy(): void;
731
887
  }
732
888
 
733
- export { type AlignValue, BLOCK_SPECS, type BlockAttrs, type BlockType, CLEAR_ALL_MARKS, COMMAND_ORIGIN, type CellAttrs, type ClipboardSlice, type CommandContext, type CommentAttr, DEFAULT_PAGE_SETTINGS, type DeltaOp, type DirValue, EditorDocument, EditorHistory, type EmbedLoc, type EncodedCursor, type EncodedCursorPoint, type ImageEmbed, type ImageLayout, type InsertContent, type LegacySerializedDocument, type LinkAttr, type ListKind, type ListStyleType, MARGIN_PRESETS, MAX_LIST_LEVEL, type MarkAttrs, type MarkName, type MarkValue, PAGE_PRESETS, type PagePreset, type PageSettings, type Position, SOFER_MIME, type Selection, type SerializedBlock, type SerializedCell, type SerializedDocument, TRACKED_ORIGINS, type TableLocation, type TableRect, cellsInRect, clearRectCells, collapsedSelection, comparePositions, createBlock, createCell, createTableBlock, decodeSelection, dedentList, defaultAttrsFor, deleteBackward, deleteForward, deleteSelection, deleteTable, deleteTableColumn, deleteTableRow, deltaLength, detectPreset, encodeSelection, expandTableRect, getBlockId, getMarksInRange, indentList, insertImage, insertParagraph, insertSlice, insertTable, insertTableColumn, insertTableRow, insertText, isCollapsed, isCommandOrigin, isImageEmbed, isKnownBlockType, isLegacySerializedDocument, isMarkUniformInRange, mergeDown, mergeRight, mergeSelection, mmToPx, moveEmbedAnchor, moveToNextCell, moveToPrevCell, newBlockId, orderedRange, positionsEqual, pxToMm, removeMark, sameTextRun, serializeSelection, setBlockAttr, setBlockType, setCellAttr, setColumnWidth, setImageAttrs, setMark, sliceDelta, sliceToInlineDelta, sliceToText, spanOf, splitCell, splitListItem, tableLocationOf, tableRectSelection, toggleList, toggleMark, withDefaults };
889
+ export { ANSWER_LINE_COLOR, ANSWER_LINE_MAX, type AlignValue, type AnswerLineSpacing, BLANK_MIN_RUN, BLANK_STYLE, BLOCK_SPECS, type BlockAttrs, type BlockType, CLEAR_ALL_MARKS, COMMAND_ORIGIN, type CellAttrs, type CellBorderColors, type CellBorderPos, type ClipboardSlice, type CommandContext, type CommentAttr, DEFAULT_PAGE_SETTINGS, type DeltaOp, type DirValue, EditorDocument, EditorHistory, type EmbedLoc, type EncodedCursor, type EncodedCursorPoint, type ImageEmbed, type ImageLayout, type InsertContent, type LegacySerializedDocument, type LinkAttr, type ListKind, type ListStyleType, MARGIN_PRESETS, MAX_LIST_LEVEL, type MarkAttrs, type MarkName, type MarkValue, PAGE_PRESETS, type PagePreset, type PageSettings, type Position, SOFER_MIME, type Selection, type SerializedBlock, type SerializedCell, type SerializedDocument, type StyleRecord, TABLE_BORDER_COLOR, TABLE_GUIDE_COLOR, TRACKED_ORIGINS, type TableBorderPreset, type TableLocation, type TableRect, answerLineStyle, cellBorderColors, cellBorderStyle, cellsInRect, clearRectCells, collapsedSelection, comparePositions, createBlock, createCell, createTableBlock, decodeSelection, dedentList, defaultAttrsFor, deleteBackward, deleteForward, deleteSelection, deleteTable, deleteTableColumn, deleteTableRow, deltaLength, detectPreset, encodeSelection, expandTableRect, getBlockId, getMarksInRange, indentList, insertAnswerLines, insertImage, insertParagraph, insertSlice, insertTable, insertTableColumn, insertTableRow, insertText, isCollapsed, isCommandOrigin, isImageEmbed, isKnownBlockType, isLegacySerializedDocument, isMarkUniformInRange, mergeDown, mergeRight, mergeSelection, mmToPx, moveEmbedAnchor, moveToNextCell, moveToPrevCell, newBlockId, orderedRange, positionsEqual, pxToMm, removeMark, sameTextRun, serializeSelection, setBlockAttr, setBlockAttrAtIndex, setBlockType, setCellAttr, setColumnWidth, setImageAttrs, setMark, sliceDelta, sliceToInlineDelta, sliceToText, spanOf, splitCell, splitListItem, splitUnderscoreRuns, styleToCssText, tableLocationOf, tableRectSelection, toggleList, toggleMark, withDefaults };
package/dist/index.js CHANGED
@@ -591,6 +591,7 @@ var ALL_MARKS = {
591
591
  underline: true,
592
592
  strike: true,
593
593
  color: true,
594
+ highlight: true,
594
595
  fontFamily: true,
595
596
  fontSize: true,
596
597
  link: true,
@@ -722,6 +723,88 @@ function sameMarkValue(a, b) {
722
723
  return false;
723
724
  }
724
725
 
726
+ // src/decorations.ts
727
+ var BLANK_MIN_RUN = 3;
728
+ function splitUnderscoreRuns(text) {
729
+ if (text.length === 0) return [];
730
+ const re = new RegExp(`_{${BLANK_MIN_RUN},}`, "g");
731
+ const out = [];
732
+ let last = 0;
733
+ let match;
734
+ while ((match = re.exec(text)) !== null) {
735
+ if (match.index > last) out.push({ text: text.slice(last, match.index), blank: false });
736
+ out.push({ text: match[0], blank: true });
737
+ last = match.index + match[0].length;
738
+ }
739
+ if (last < text.length) out.push({ text: text.slice(last), blank: false });
740
+ return out;
741
+ }
742
+ var BLANK_STYLE = {
743
+ WebkitTextFillColor: "transparent",
744
+ borderBottom: "1px solid currentColor"
745
+ };
746
+ var ANSWER_LINE_COLOR = "#000000";
747
+ function answerLineStyle(attrs) {
748
+ if (attrs.answerLine !== true) return void 0;
749
+ const spacing = attrs.answerLineSpacing ?? 1;
750
+ return {
751
+ borderBottom: `1px solid ${ANSWER_LINE_COLOR}`,
752
+ lineHeight: String(spacing),
753
+ // `minHeight` casado com a entrelinha, e NÃO redundante: o CSS do
754
+ // consumidor costuma trazer `min-height` em parágrafo (o playground usa
755
+ // 1.5em "para manter parágrafo vazio visível"). Sem esta linha, "Simples"
756
+ // e "1,5" renderizariam com a mesma altura.
757
+ minHeight: `${spacing}em`
758
+ };
759
+ }
760
+ function styleToCssText(style) {
761
+ return Object.entries(style).map(([k, v]) => `${kebab(k)}:${v}`).join(";");
762
+ }
763
+ function kebab(prop) {
764
+ if (prop.startsWith("--")) return prop;
765
+ return prop.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
766
+ }
767
+
768
+ // src/table-borders.ts
769
+ var TABLE_BORDER_COLOR = "#cbd5e1";
770
+ var TABLE_GUIDE_COLOR = "var(--ed-guide-color, transparent)";
771
+ function cellBorderColors(preset, pos, variant) {
772
+ const on = TABLE_BORDER_COLOR;
773
+ const off = variant === "screen" ? TABLE_GUIDE_COLOR : "transparent";
774
+ switch (preset ?? "all") {
775
+ case "horizontal":
776
+ return { top: on, right: off, bottom: on, left: off };
777
+ case "vertical":
778
+ return { top: off, right: on, bottom: off, left: on };
779
+ case "none":
780
+ return { top: off, right: off, bottom: off, left: off };
781
+ case "outer": {
782
+ const isTop = pos.row === pos.rowStart;
783
+ const isBottom = pos.row + Math.max(1, pos.rowspan) - 1 === pos.rowEnd - 1;
784
+ const isLeft = pos.col === 0;
785
+ const isRight = pos.col + Math.max(1, pos.colspan) - 1 === pos.cols - 1;
786
+ return {
787
+ top: isTop ? on : off,
788
+ right: isRight ? on : off,
789
+ bottom: isBottom ? on : off,
790
+ left: isLeft ? on : off
791
+ };
792
+ }
793
+ case "all":
794
+ default:
795
+ return { top: on, right: on, bottom: on, left: on };
796
+ }
797
+ }
798
+ function cellBorderStyle(preset, pos, variant) {
799
+ const c = cellBorderColors(preset, pos, variant);
800
+ return {
801
+ borderTopColor: c.top,
802
+ borderRightColor: c.right,
803
+ borderBottomColor: c.bottom,
804
+ borderLeftColor: c.left
805
+ };
806
+ }
807
+
725
808
  // src/commands.ts
726
809
  import * as Y3 from "yjs";
727
810
 
@@ -1046,6 +1129,16 @@ function setBlockAttr(ctx, key, value) {
1046
1129
  ctx.setSelection(sel);
1047
1130
  });
1048
1131
  }
1132
+ function setBlockAttrAtIndex(ctx, blockIndex, key, value) {
1133
+ transact(ctx.doc, () => {
1134
+ const block = ctx.doc.getBlock(blockIndex);
1135
+ if (!block) return;
1136
+ const attrsMap = block.get("attrs") ?? new Y3.Map();
1137
+ if (!block.get("attrs")) block.set("attrs", attrsMap);
1138
+ if (value === null || value === void 0) attrsMap.delete(key);
1139
+ else attrsMap.set(key, value);
1140
+ });
1141
+ }
1049
1142
  function setCellAttr(ctx, key, value) {
1050
1143
  transact(ctx.doc, () => {
1051
1144
  const sel = ctx.getSelection();
@@ -1314,6 +1407,19 @@ function clampLevel(l) {
1314
1407
  if (typeof l !== "number" || !Number.isFinite(l)) return 0;
1315
1408
  return Math.max(0, Math.min(MAX_LIST_LEVEL, Math.trunc(l)));
1316
1409
  }
1410
+ var ANSWER_LINE_MAX = 50;
1411
+ function insertAnswerLines(ctx, count, spacing) {
1412
+ const n = Math.max(1, Math.min(ANSWER_LINE_MAX, Math.trunc(count)));
1413
+ transact(ctx.doc, () => {
1414
+ const sel = ctx.getSelection();
1415
+ const focusBlock = Math.max(0, Math.min(sel.focus.blockIndex, ctx.doc.blockCount() - 1));
1416
+ const insertAt = focusBlock + 1;
1417
+ const attrs = { answerLine: true, answerLineSpacing: spacing };
1418
+ const blocks = Array.from({ length: n }, () => createBlock("paragraph", "", attrs));
1419
+ ctx.doc.blocks.insert(insertAt, blocks);
1420
+ ctx.setSelection(collapsedSelection({ blockIndex: insertAt, offset: 0 }));
1421
+ });
1422
+ }
1317
1423
  function insertTable(ctx, rows, cols) {
1318
1424
  const r = Math.max(1, Math.trunc(rows));
1319
1425
  const c = Math.max(1, Math.trunc(cols));
@@ -2061,6 +2167,10 @@ var EditorHistory = class {
2061
2167
  }
2062
2168
  };
2063
2169
  export {
2170
+ ANSWER_LINE_COLOR,
2171
+ ANSWER_LINE_MAX,
2172
+ BLANK_MIN_RUN,
2173
+ BLANK_STYLE,
2064
2174
  BLOCK_SPECS,
2065
2175
  CLEAR_ALL_MARKS,
2066
2176
  COMMAND_ORIGIN,
@@ -2071,8 +2181,13 @@ export {
2071
2181
  MAX_LIST_LEVEL,
2072
2182
  PAGE_PRESETS,
2073
2183
  SOFER_MIME,
2184
+ TABLE_BORDER_COLOR,
2185
+ TABLE_GUIDE_COLOR,
2074
2186
  TRACKED_ORIGINS,
2075
2187
  Y3 as Y,
2188
+ answerLineStyle,
2189
+ cellBorderColors,
2190
+ cellBorderStyle,
2076
2191
  cellsInRect,
2077
2192
  clearRectCells,
2078
2193
  collapsedSelection,
@@ -2096,6 +2211,7 @@ export {
2096
2211
  getBlockId,
2097
2212
  getMarksInRange,
2098
2213
  indentList,
2214
+ insertAnswerLines,
2099
2215
  insertImage,
2100
2216
  insertParagraph,
2101
2217
  insertSlice,
@@ -2124,6 +2240,7 @@ export {
2124
2240
  sameTextRun,
2125
2241
  serializeSelection,
2126
2242
  setBlockAttr,
2243
+ setBlockAttrAtIndex,
2127
2244
  setBlockType,
2128
2245
  setCellAttr,
2129
2246
  setColumnWidth,
@@ -2135,6 +2252,8 @@ export {
2135
2252
  spanOf,
2136
2253
  splitCell,
2137
2254
  splitListItem,
2255
+ splitUnderscoreRuns,
2256
+ styleToCssText,
2138
2257
  tableLocationOf,
2139
2258
  tableRectSelection,
2140
2259
  toggleList,