Skip to content

Commit

Permalink
Merge pull request #822 from streamich/peritext-inline-formatting
Browse files Browse the repository at this point in the history
Peritext basic inline formatting
  • Loading branch information
streamich authored Feb 15, 2025
2 parents 8c4569c + 853ec06 commit 53201cb
Show file tree
Hide file tree
Showing 19 changed files with 308 additions and 61 deletions.
61 changes: 48 additions & 13 deletions src/json-crdt-extensions/peritext/block/Inline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {stringify} from '../../../json-text/stringify';
import {SliceBehavior, SliceTypeName} from '../slice/constants';
import {Range} from '../rga/Range';
import {ChunkSlice} from '../util/ChunkSlice';
import {MarkerOverlayPoint} from '../overlay/MarkerOverlayPoint';
import {Cursor} from '../editor/Cursor';
import {hashId} from '../../../json-crdt/hash';
import {formatType} from '../slice/util';
Expand All @@ -15,34 +14,70 @@ import type {Peritext} from '../Peritext';
import type {Slice} from '../slice/types';
import type {PeritextMlAttributes, PeritextMlNode} from './types';

/** The attribute started before this inline and ends after this inline. */
export class InlineAttrPassing {
export abstract class AbstractInlineAttr {
constructor(public slice: Slice) {}

/** @returns Whether the attribute starts at the start of the inline. */
isStart(): boolean {
return false;
}

/** @returns Whether the attribute ends at the end of the inline. */
isEnd(): boolean {
return false;
}

/** @returns Whether the attribute is collapsed to a point. */
isCollapsed(): boolean {
return false;
}
}

/** The attribute started before this inline and ends after this inline. */
export class InlineAttrPassing extends AbstractInlineAttr {}

/** The attribute starts at the beginning of this inline. */
export class InlineAttrStart {
constructor(public slice: Slice) {}
export class InlineAttrStart extends AbstractInlineAttr {
isStart(): boolean {
return true;
}
}

/** The attribute ends at the end of this inline. */
export class InlineAttrEnd {
constructor(public slice: Slice) {}
export class InlineAttrEnd extends AbstractInlineAttr {
isEnd(): boolean {
return true;
}
}

/** The attribute starts and ends in this inline, exactly contains it. */
export class InlineAttrContained {
constructor(public slice: Slice) {}
export class InlineAttrContained extends AbstractInlineAttr {
isStart(): boolean {
return true;
}
isEnd(): boolean {
return true;
}
}

/** The attribute is collapsed at start of this inline. */
export class InlineAttrStartPoint {
constructor(public slice: Slice) {}
export class InlineAttrStartPoint extends AbstractInlineAttr {
isStart(): boolean {
return true;
}
isCollapsed(): boolean {
return true;
}
}

/** The attribute is collapsed at end of this inline. */
export class InlineAttrEndPoint {
constructor(public slice: Slice) {}
export class InlineAttrEndPoint extends AbstractInlineAttr {
isEnd(): boolean {
return true;
}
isCollapsed(): boolean {
return true;
}
}

export type InlineAttr =
Expand Down
2 changes: 1 addition & 1 deletion src/json-crdt-extensions/peritext/block/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export {Block, IBlock} from './Block';
export {LeafBlock} from './LeafBlock';
export {Inline} from './Inline';
export * from './Inline';
8 changes: 5 additions & 3 deletions src/json-crdt-extensions/peritext/slice/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const enum SliceTypeCon {
collapselist = 26, // Collapsible list - > List item
collapse = 27, // Collapsible block
note = 28, // Note block
mathblock = 29, // <math> block

// ------------------------------------------------ inline slices (-64 to -1)
Cursor = -1,
Expand All @@ -56,12 +57,12 @@ export const enum SliceTypeCon {
ins = -12, // <ins>
sup = -13, // <sup>
sub = -14, // <sub>
math = -15, // <math>
math = -15, // <math> inline
font = -16, // <span style="font-family: ...">
col = -17, // <span style="color: ...">
bg = -18, // <span style="background: ...">
kbd = -19, // <kbd>
hidden = -20, // <span style="color: transparent; background: black">
spoiler = -20, // <span style="color: transparent; background: black">
q = -21, // <q> (inline quote)
cite = -22, // <cite> (inline citation)
footnote = -23, // <sup> or <a> with href="#footnote-..." and title="Footnote ..."
Expand Down Expand Up @@ -106,6 +107,7 @@ export enum SliceTypeName {
collapselist = SliceTypeCon.collapselist,
collapse = SliceTypeCon.collapse,
note = SliceTypeCon.note,
mathblock = SliceTypeCon.mathblock,

Cursor = SliceTypeCon.Cursor,
RemoteCursor = SliceTypeCon.RemoteCursor,
Expand All @@ -126,7 +128,7 @@ export enum SliceTypeName {
col = SliceTypeCon.col,
bg = SliceTypeCon.bg,
kbd = SliceTypeCon.kbd,
hidden = SliceTypeCon.hidden,
spoiler = SliceTypeCon.spoiler,
footnote = SliceTypeCon.footnote,
ref = SliceTypeCon.ref,
iaside = SliceTypeCon.iaside,
Expand Down
7 changes: 7 additions & 0 deletions src/json-crdt-peritext-ui/plugins/cursor/RenderCaret.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ const innerClass = rule({
an: moveAnimation + ' .25s ease-out forwards',
});

const innerClass2 = rule({
'mix-blend-mode': 'hard-light',
});

export interface RenderCaretProps extends CaretViewProps {
children: React.ReactNode;
}
Expand Down Expand Up @@ -84,6 +88,9 @@ export const RenderCaret: React.FC<RenderCaretProps> = ({italic, children}) => {
}}
/>
)}

{/* Two carets overlay, so that they look good, both, on white and black backgrounds. */}
<span className={innerClass + innerClass2} style={style} />
<span className={innerClass} style={style} />
</span>
);
Expand Down
9 changes: 8 additions & 1 deletion src/json-crdt-peritext-ui/plugins/cursor/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
export enum DefaultRendererColors {
ActiveCursor = '#07f',
InactiveCursor = 'rgba(127,127,127,.7)',
ActiveSelection = '#d7e9fd',

/**
* Derived from #d7e9fd. 80% opacity used so that
* any inline formatting underneath the selection
* is still visible.
*/
ActiveSelection = 'rgba(196,223,253,.8)',

InactiveSelection = 'rgba(127,127,127,.2)',
}
23 changes: 23 additions & 0 deletions src/json-crdt-peritext-ui/plugins/minimal/RenderInline/Spoiler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// biome-ignore lint: React is used for JSX
import * as React from 'react';
import {rule} from 'nano-theme';

const blockClass = rule({
bg: '#222',
col: 'transparent',
bdrad: '2px',
'&:hover': {
bg: '#222',
col: 'rgba(255, 255, 255, 0.2)',
},
});

export interface SpoilerProps {
children: React.ReactNode;
}

export const Spoiler: React.FC<SpoilerProps> = (props) => {
const {children} = props;

return <span className={blockClass}>{children}</span>;
};
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// biome-ignore lint: React is used for JSX
import * as React from 'react';
import {usePeritext} from '../../react';
import {useSyncStoreOpt} from '../../react/hooks';
import {DefaultRendererColors} from './constants';
import type {InlineViewProps} from '../../react/InlineView';
import {CommonSliceType} from '../../../json-crdt-extensions';
import {usePeritext} from '../../../react';
import {useSyncStoreOpt} from '../../../react/hooks';
import {DefaultRendererColors} from '../constants';
import {CommonSliceType} from '../../../../json-crdt-extensions';
import {Spoiler} from './Spoiler';
import type {InlineViewProps} from '../../../react/InlineView';

interface RenderInlineSelectionProps extends RenderInlineProps {
selection: [left: 'anchor' | 'focus' | '', right: 'anchor' | 'focus' | ''];
Expand Down Expand Up @@ -44,8 +45,7 @@ export const RenderInline: React.FC<RenderInlineProps> = (props) => {
if (attr[CommonSliceType.sub]) element = <sub>{element}</sub>;
if (attr[CommonSliceType.math]) element = <code>{element}</code>;
if (attr[CommonSliceType.kbd]) element = <kbd>{element}</kbd>;
if (attr[CommonSliceType.hidden])
element = <span style={{color: 'transparent', background: 'black'}}>{element}</span>;
if (attr[CommonSliceType.spoiler]) element = <Spoiler>{element}</Spoiler>;

if (selection) {
element = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const TopToolbar: React.FC<TopToolbarProps> = ({ctx}) => {
{inlineGroupButton(CommonSliceType.b, 'Bold')}
{inlineGroupButton(CommonSliceType.i, 'Italic')}
{inlineGroupButton(CommonSliceType.u, 'Underline')}
{inlineGroupButton(CommonSliceType.overline, 'Overline')}
{inlineGroupButton(CommonSliceType.s, 'Strikethrough')}
{inlineGroupButton(CommonSliceType.code, 'Code')}
{inlineGroupButton(CommonSliceType.mark, 'Mark')}
Expand All @@ -60,7 +61,7 @@ export const TopToolbar: React.FC<TopToolbarProps> = ({ctx}) => {
{inlineGroupButton(CommonSliceType.sub, 'Subscript')}
{inlineGroupButton(CommonSliceType.math, 'Math')}
{inlineGroupButton(CommonSliceType.kbd, 'Key')}
{inlineGroupButton(CommonSliceType.hidden, 'Spoiler')}
{inlineGroupButton(CommonSliceType.spoiler, 'Spoiler')}
{inlineGroupButton(CommonSliceType.bookmark, 'Bookmark')}
<ButtonSeparator />
{button('Blue', () => {
Expand Down
1 change: 1 addition & 0 deletions src/json-crdt-peritext-ui/plugins/minimal/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const text: PeritextPlugin['text'] = (props, inline) => {
if (attrs[CommonSliceType.b]) style.fontWeight = 'bold';
if (attrs[CommonSliceType.i]) style.fontStyle = 'italic';
if (attrs[CommonSliceType.u]) textDecoration = 'underline';
if (attrs[CommonSliceType.overline]) textDecoration = textDecoration ? textDecoration + ' overline' : 'overline';
if (attrs[CommonSliceType.s]) textDecoration = textDecoration ? textDecoration + ' line-through' : 'line-through';
if ((attr = attrs[CommonSliceType.col])) style.color = attr[0].slice.data() + '';

Expand Down
2 changes: 1 addition & 1 deletion src/json-crdt-peritext-ui/plugins/toolbar/RenderBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// biome-ignore lint: React is used for JSX
import * as React from 'react';
import type {BlockViewProps} from '../../react/BlockView';
import {CommonSliceType} from '../../../json-crdt-extensions';
import type {BlockViewProps} from '../../react/BlockView';

export interface RenderBlockProps extends BlockViewProps {
children: React.ReactNode;
Expand Down
25 changes: 0 additions & 25 deletions src/json-crdt-peritext-ui/plugins/toolbar/RenderInline.tsx

This file was deleted.

41 changes: 41 additions & 0 deletions src/json-crdt-peritext-ui/plugins/toolbar/RenderInline/Code.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// biome-ignore lint: React is used for JSX
import * as React from 'react';
import {rule, drule, theme, useTheme} from 'nano-theme';
import type {InlineAttr} from '../../../../json-crdt-extensions';

const blockClass = drule({
...theme.font.mono.mid,
fz: '.9em',
pdt: '.05em',
pdb: '.05em',
});

const startClass = rule({
borderTopLeftRadius: '.3em',
borderBottomLeftRadius: '.3em',
pdl: '.24em',
});

const endClass = rule({
borderTopRightRadius: '.3em',
borderBottomRightRadius: '.3em',
pdr: '.24em',
});

export interface CodeProps {
attr: InlineAttr;
children: React.ReactNode;
}

export const Code: React.FC<CodeProps> = (props) => {
const {children, attr} = props;
const theme = useTheme();
const className =
blockClass({
bg: theme.g(0.2, 0.1),
}) +
(attr.isStart() ? startClass : '') +
(attr.isEnd() ? endClass : '');

return <span className={className}>{children}</span>;
};
19 changes: 19 additions & 0 deletions src/json-crdt-peritext-ui/plugins/toolbar/RenderInline/Del.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// biome-ignore lint: React is used for JSX
import * as React from 'react';
import {rule} from 'nano-theme';

const delClass = rule({
bg: '#ffebe9',
bxsh: '0 2px 0 0 #ffcecb',
col: 'red',
});

export interface DelProps {
children: React.ReactNode;
}

export const Del: React.FC<DelProps> = (props) => {
const {children} = props;

return <del className={delClass}>{children}</del>;
};
19 changes: 19 additions & 0 deletions src/json-crdt-peritext-ui/plugins/toolbar/RenderInline/Ins.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// biome-ignore lint: React is used for JSX
import * as React from 'react';
import {rule} from 'nano-theme';

const blockClass = rule({
bg: '#dafbe1',
bxsh: '0 2px 0 0 #aceebb',
td: 'none',
});

export interface InsProps {
children: React.ReactNode;
}

export const Ins: React.FC<InsProps> = (props) => {
const {children} = props;

return <ins className={blockClass}>{children}</ins>;
};
Loading

0 comments on commit 53201cb

Please sign in to comment.