Skip to content

Commit

Permalink
Merge pull request #753 from streamich/peritext-inline-annotations
Browse files Browse the repository at this point in the history
Peritext inline boolean ("Overwrite") annotations
  • Loading branch information
streamich authored Nov 7, 2024
2 parents a5cfb46 + 8f242a7 commit 676f0ed
Show file tree
Hide file tree
Showing 18 changed files with 225 additions and 67 deletions.
4 changes: 4 additions & 0 deletions src/json-crdt-extensions/peritext/block/Inline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ export class Inline extends Range implements Printable {
/**
* @returns Returns the attributes of the inline, which are the slice
* annotations and formatting applied to the inline.
*
* @todo Rename to `.stat()`.
* @todo Create a more efficient way to compute inline stats, separate: (1)
* boolean flags, (2) cursor, (3) other attributes.
*/
public attr(): InlineAttrs {
if (this._attr) return this._attr;
Expand Down
3 changes: 3 additions & 0 deletions src/json-crdt-extensions/peritext/block/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export {Block, IBlock} from './Block';
export {LeafBlock} from './LeafBlock';
export {Inline} from './Inline';
2 changes: 2 additions & 0 deletions src/json-crdt-extensions/peritext/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {Extension} from '../../json-crdt/extensions/Extension';
import type {PeritextDataNode} from './types';

export {PeritextNode, PeritextApi, Peritext};
export * from './slice';
export * from './block';

export const peritext = new Extension<
ExtensionId.peritext,
Expand Down
8 changes: 7 additions & 1 deletion src/json-crdt-extensions/peritext/overlay/Overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,13 @@ export class Overlay<T = string> implements Printable, Stateful {
range: Range<T>,
endOnMarker = 10,
): [complete: Set<SliceType>, partial: Set<SliceType>, markerCount: number] {
const {start, end} = range;
const {start, end: end_} = range;
let end = end_;
const isSamePoint = start.cmp(end_) === 0;
if (isSamePoint) {
end = end.clone();
end.halfstep(1);
}
const after = this.getOrNextLower(start);
const hasLeadingPoint = !!after;
const iterator = this.points0(after, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,38 @@ const statTestSuite = (setup: () => Kit) => {
assert(12, 7, [], ['bold']);
assert(21, 2, [], []);
});

test('caret with overlapping slices and erasure', () => {
const {peritext, editor} = setup();
editor.cursor.setAt(5, 10);
editor.saved.insOverwrite('bold');
editor.cursor.setAt(10, 10);
editor.saved.insOverwrite('italic');
editor.cursor.setAt(7, 2);
editor.saved.insErase('bold');
peritext.refresh();
const assert = (at: number, complete: unknown[], partial: unknown[]) => {
const expected = [new Set(complete), new Set(partial), 0];
const range = peritext.rangeAt(at);
expect(peritext.overlay.stat(range)).toEqual(expected);
editor.cursor.setAt(at);
expect(peritext.overlay.stat(editor.cursor)).toEqual(expected);
peritext.refresh();
expect(peritext.overlay.stat(editor.cursor)).toEqual(expected);
editor.delCursors();
};
assert(0, [], []);
assert(2, [], []);
assert(6, ['bold'], []);
assert(8, [], []);
assert(9, ['bold'], []);
assert(10, ['bold'], []);
assert(11, ['bold', 'italic'], []);
assert(13, ['bold', 'italic'], []);
assert(16, ['italic'], []);
assert(20, [], []);
assert(21, [], []);
});
};

describe('.stat()', () => {
Expand Down
77 changes: 45 additions & 32 deletions src/json-crdt-extensions/peritext/slice/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,41 +13,54 @@ export enum CursorAnchor {
* Built-in slice types.
*/
export enum CommonSliceType {
Cursor = -1,

// Block slices
Paragraph = 0,
Title = 1,
H1 = 1,
H2 = 2,
H3 = 3,
H4 = 4,
H5 = 5,
H6 = 6,
BlockQuote = 7,
CodeBlock = 8,
Preformatted = 9,
OrderedList = 10,
UnorderedList = 11,
TaskList = 12,
ListItem = 13,
LineBreak = 14,
NewLine = 15,
PageBreak = 16,
Aside = 17,
p = 0, // <p>
blockquote = 1, // <blockquote>
codeblock = 2, // <pre><code>
pre = 3, // <pre>
ul = 4, // <ul>
ol = 5, // <ol>
TaskList = 6, // - [ ] Task list
h1 = 7, // <h1>
h2 = 8, // <h2>
h3 = 9, // <h3>
h4 = 10, // <h4>
h5 = 11, // <h5>
h6 = 12, // <h6>
title = 13, // <title>
subtitle = 14, // <subtitle>
br = 15, // <br>
nl = 16, // \n
hr = 17, // <hr>
page = 18, // Page break
aside = 19, // <aside>
embed = 20, // <embed>, <iframe>, <object>, <video>, <audio>, etc.

// Inline slices
Bold = -2,
Italic = -3,
Underline = -4,
Strikethrough = -5,
Code = -6,
Highlight = -7,
Link = -8,
Comment = -9,
Superscript = -10,
Subscript = -11,
Math = -12,
Cursor = -1,
RemoteCursor = -2,
b = -3, // <b>
i = -4, // <i>
u = -5, // <u>
s = -6, // <s>
code = -7, // <code>
mark = -8, // <mark>
a = -9, // <a>
comment = -10, // User comment attached to a slice
del = -11, // <del>
ins = -12, // <ins>
sup = -13, // <sup>
sub = -14, // <sub>
math = -15, // <math>
font = -16, // <span style="font-family: ...">
col = -17, // <span style="color: ...">
bg = -18, // <span style="background: ...">
hidden = -19, // <span style="color: transparent; background: black">
footnote = -20, // <sup> or <a> with href="#footnote-..." and title="Footnote ..."
ref = -21, // <a> with href="#ref-..." and title="Reference ..."
iaside = -22, // Inline <aside>
iembed = -23, // inline embed (any media, dropdown, Google Docs-like chips: date, person, file, etc.)
bookmark = -24, // UI for creating a link to this slice
}

export enum SliceHeaderMask {
Expand Down
2 changes: 2 additions & 0 deletions src/json-crdt-extensions/peritext/slice/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type * from './types';
export {CursorAnchor, CommonSliceType} from './constants';
2 changes: 1 addition & 1 deletion src/json-crdt-peritext-ui/dom/DomController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class DomController implements UiLifeCycles, Printable {
const et = (this.et = new PeritextEventTarget());
const defaults = new PeritextEventDefaults(txt, et);
et.defaults = defaults;
const keys = (this.keys = new KeyController());
const keys = (this.keys = new KeyController({source}));
const comp = (this.comp = new CompositionController({et, source, txt}));
this.input = new InputController({et, source, txt, comp});
this.cursor = new CursorController({et, source, txt, keys});
Expand Down
8 changes: 8 additions & 0 deletions src/json-crdt-peritext-ui/dom/KeyController.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type {Printable} from 'tree-dump';
import type {UiLifeCycles} from './types';

export interface KeyControllerOpts {
source: HTMLElement;
}

/**
* Keeps track of all pressed down keys.
*/
Expand All @@ -10,12 +14,15 @@ export class KeyController implements UiLifeCycles, Printable {
*/
public readonly pressed = new Set<string>();

public constructor(protected readonly opts: KeyControllerOpts) {}

public start(): void {
document.addEventListener('keydown', this.onKeyDown);
document.addEventListener('keyup', this.onKeyUp);
document.addEventListener('focus', this.onReset);
document.addEventListener('compositionstart', this.onReset);
document.addEventListener('compositionend', this.onReset);
this.opts.source.addEventListener('blur', this.onReset);
}

public stop(): void {
Expand All @@ -24,6 +31,7 @@ export class KeyController implements UiLifeCycles, Printable {
document.removeEventListener('focus', this.onReset);
document.removeEventListener('compositionstart', this.onReset);
document.removeEventListener('compositionend', this.onReset);
this.opts.source.removeEventListener('blur', this.onReset);
}

private onKeyDown = (event: KeyboardEvent): void => {
Expand Down
32 changes: 23 additions & 9 deletions src/json-crdt-peritext-ui/dom/RichTextController.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type {PeritextEventTarget} from '../events/PeritextEventTarget';
import type {UiLifeCycles} from './types';
import type {Peritext} from '../../json-crdt-extensions/peritext';
import {CommonSliceType, type Peritext} from '../../json-crdt-extensions/peritext';

export interface RichTextControllerOpts {
source: HTMLElement;
Expand All @@ -27,15 +27,29 @@ export class RichTextController implements UiLifeCycles {
const key = event.key;
if (event.isComposing || key === 'Dead') return;
const et = this.opts.et;
if (key === 'b' && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
et.inline({type: 'b'});
return;
if (event.metaKey || event.ctrlKey) {
switch (key) {
case 'b':
event.preventDefault();
et.inline(CommonSliceType.b);
return;
case 'i':
event.preventDefault();
et.inline(CommonSliceType.i);
return;
case 'u':
event.preventDefault();
et.inline(CommonSliceType.u);
return;
}
}
if (key === 'i' && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
et.inline({type: 'i'});
return;
if (event.metaKey && event.shiftKey) {
switch (key) {
case 'x':
event.preventDefault();
et.inline(CommonSliceType.s);
return;
}
}
};
}
16 changes: 15 additions & 1 deletion src/json-crdt-peritext-ui/events/PeritextEventDefaults.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {Peritext} from '../../json-crdt-extensions/peritext';
import type {EditorSlices} from '../../json-crdt-extensions/peritext/editor/EditorSlices';
import {Anchor} from '../../json-crdt-extensions/peritext/rga/constants';
import {CursorAnchor} from '../../json-crdt-extensions/peritext/slice/constants';
import type {PeritextEventHandlerMap, PeritextEventTarget} from './PeritextEventTarget';
import type * as events from './types';
Expand Down Expand Up @@ -64,6 +65,7 @@ export class PeritextEventDefaults implements PeritextEventHandlerMap {
}
// Set caret (a collapsed cursor) at the specified position.
else {
point.refAfter();
editor.cursor.set(point);
if (unit) editor.select(unit);
}
Expand Down Expand Up @@ -98,7 +100,7 @@ export class PeritextEventDefaults implements PeritextEventHandlerMap {
};

public readonly inline = (event: CustomEvent<events.InlineDetail>) => {
const {type, store = 'saved', behavior = 'overwrite', data, pos} = event.detail;
const {type, store = 'saved', behavior = 'overwrite', data} = event.detail;
const editor = this.txt.editor;
const slices: EditorSlices = store === 'saved' ? editor.saved : store === 'extra' ? editor.extra : editor.local;
switch (behavior) {
Expand All @@ -109,6 +111,18 @@ export class PeritextEventDefaults implements PeritextEventHandlerMap {
slices.insErase(type, data);
break;
default:
for (let i = editor.cursors0(), cursor = i(); cursor; cursor = i()) {
// For inline boolean slices, ref endpoint "before" the next character
// as per the Peritext paper, so that, say, bold text automatically
// includes the next character typed.
if (cursor.end.anchor !== Anchor.Before || cursor.start.anchor !== Anchor.Before) {
const start = cursor.start.clone();
const end = cursor.end.clone();
start.refBefore();
end.refBefore();
cursor.set(start, end);
}
}
slices.insOverwrite(type, data);
}
};
Expand Down
10 changes: 9 additions & 1 deletion src/json-crdt-peritext-ui/events/PeritextEventTarget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,15 @@ export class PeritextEventTarget extends TypedEventTarget<PeritextEventMap> {
this.cursor({len, unit, edge});
}

public inline(detail: InlineDetail): void {
public inline(type: InlineDetail['type'], behavior?: InlineDetail['behavior'], data?: InlineDetail['data']): void;
public inline(detail: InlineDetail): void;
public inline(
a: InlineDetail | InlineDetail['type'],
behavior?: InlineDetail['behavior'],
data?: InlineDetail['data'],
): void {
const detail: InlineDetail =
typeof a === 'object' && !Array.isArray(a) ? (a as InlineDetail) : ({type: a, behavior, data} as InlineDetail);
this.dispatch('inline', detail);
}
}
4 changes: 2 additions & 2 deletions src/json-crdt-peritext-ui/react/InlineView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {CaretView} from './selection/CaretView';
import {FocusView} from './selection/FocusView';
import {AnchorView} from './selection/AnchorView';
import {InlineAttrEnd, InlineAttrStart, type Inline} from '../../json-crdt-extensions/peritext/block/Inline';
import {CommonSliceType} from '../../json-crdt-extensions';
import type {SpanProps} from './types';

const {createElement: h, Fragment} = React;
Expand Down Expand Up @@ -73,10 +74,9 @@ export const InlineView: React.FC<InlineViewProps> = (props) => {
if (inline.hasCursor()) {
const elements: React.ReactNode[] = [];
const attr = inline.attr();
const italic = attr.i && attr.i[0];
const italic = attr[CommonSliceType.i] && attr[CommonSliceType.i][0];
const key = inline.key();
const cursorStart = inline.cursorStart();
console.log('italic', italic);
if (cursorStart) {
const k = key + 'a';
elements.push(
Expand Down
17 changes: 9 additions & 8 deletions src/json-crdt-peritext-ui/renderers/default/Button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,15 @@ import * as React from 'react';
import {drule} from 'nano-theme';

const blockClass = drule({
bdrad: '.5rem',
bd: 0,
col: 'black',
ff: 'inherit',
fz: '.875rem',
fz: '14px',
fw: 500,
lh: '1.15em',
mr: 'none',
pd: '.375rem .625rem',
trs: 'all .2s cubic-bezier(.65,.05,.36,1)',
'&:hover': {
background: 'rgba(61, 37, 20, .12)',
},
pd: '.375em .625em',
trs: 'all .15s cubic-bezier(.65,.05,.36,1)',
});

export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
Expand All @@ -27,7 +23,12 @@ export const Button: React.FC<ButtonProps> = ({active, children, ...rest}) => {
const className =
(rest.className || '') +
blockClass({
background: active ? 'rgba(61, 37, 20, .12)' : 'rgba(61, 37, 20, .08)',
bdrad: active ? '.6em' : '.4em',
bg: active ? '#07f' : 'rgba(61, 37, 20, .08)',
col: active ? 'white' : 'black',
'&:hover': {
bg: active ? '#06e' : 'rgba(61, 37, 20, .12)',
},
});

return (
Expand Down
5 changes: 4 additions & 1 deletion src/json-crdt-peritext-ui/renderers/default/Chrome/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ const blockClass = rule({
bxz: 'border-box',
bdrad: '16px',
pad: '24px 32px',
bxsh: '0 1px 8px #00000005, 0 1px 4px #00000008, 0 4px 10px #0000000a',
bxsh: '0 1px 8px #00000008,0 1px 4px #0000000a,0 4px 10px #0000000f',
'&:hover': {
bxsh: '0 1px 8px #00000008,0 1px 4px #0000000a,0 4px 10px #0000000f,0 0 3px #0000001f',
},
});

export interface ChromeProps {
Expand Down
Loading

0 comments on commit 676f0ed

Please sign in to comment.