Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions demo/components/MoveToLine.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {useState} from 'react';

import {Button, type DOMProps, Flex} from '@gravity-ui/uikit';

import {NumberInput} from 'src/index';

export type MoveToLineProps = DOMProps & {
onClick: (value: number | undefined) => void;
};

export const MoveToLine: React.FC<MoveToLineProps> = function MoveToLine({
style,
className,
onClick,
}) {
const [line, setLine] = useState<number | undefined>(0);

return (
<Flex gap="1" style={style} className={className}>
<NumberInput size="s" value={line} onUpdate={setLine} min={0} style={{width: '56px'}} />
<Button size="s" onClick={() => onClick(line)}>
Move to line
</Button>
</Flex>
);
};
55 changes: 55 additions & 0 deletions demo/stories/examples/line-numbers/Editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {memo} from 'react';

import {MarkdownEditorView, useMarkdownEditor} from 'src/index';

import {MoveToLine} from '../../../components/MoveToLine';
import {PlaygroundLayout} from '../../../components/PlaygroundLayout';
import {markup} from '../../../defaults/content';

import {lineNumbersPlugin} from './md-plugin';

export type EditorWithLineNumbersProps = {};

export const EditorWithLineNumbers = memo<EditorWithLineNumbersProps>(
function EditorWithLineNumbers() {
const editor = useMarkdownEditor(
{
initial: {
mode: 'wysiwyg',
markup,
},
wysiwygConfig: {
extensions: (builder) =>
builder.configureMd((md) => md.use(lineNumbersPlugin), {text: false}),
},
},
[],
);

return (
<PlaygroundLayout
title="Line numbers example"
editor={editor}
actions={({className}) => (
<MoveToLine
className={className}
onClick={(line) => {
if (typeof line !== 'number' || Number.isNaN(line)) return;
editor.moveCursor({line});
editor.focus();
}}
/>
)}
view={({className}) => (
<MarkdownEditorView
autofocus
stickyToolbar
settingsVisible
editor={editor}
className={className}
/>
)}
/>
);
},
);
13 changes: 13 additions & 0 deletions demo/stories/examples/line-numbers/LineNumbers.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type {StoryObj} from '@storybook/react';

import {EditorWithLineNumbers as component} from './Editor';

export const Story: StoryObj<typeof component> = {
args: {},
};
Story.storyName = 'Line Numbers';

export default {
title: 'Examples / Line Numbers',
component,
};
32 changes: 32 additions & 0 deletions demo/stories/examples/line-numbers/md-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type {PluginSimple} from 'markdown-it';
import type {RuleCore} from 'markdown-it/lib/parser_core';

const RULE_NAME = 'line_numbers';

const TOKENS: readonly string[] = [
'paragraph_open',
'heading_open',
'code_block',
'fence',
'list_item_open',
'tr_open',
'dt_open',
'checkbox_open',
'yfm_note_open',
'yfm_cut_open',
];

export const lineNumbersPlugin: PluginSimple = (md) => {
const rule: RuleCore = ({tokens}) => {
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];

if (token.map && TOKENS.includes(token.type)) {
const line = token.map[0];
token.attrPush(['data-line', String(line)]);
}
}
};

md.core.ruler.push(RULE_NAME, rule);
};
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ export const CodeBlockHighlight: ExtensionAuto<CodeBlockHighlightOptions> = (bui
let prevLang = node.attrs[CodeBlockNodeAttr.Lang];

const dom = document.createElement('pre');
updateDomAttribute(
dom,
CodeBlockNodeAttr.Line,
node.attrs[CodeBlockNodeAttr.Line],
);

const contentDOM = document.createElement('code');
contentDOM.classList.add('hljs');
Expand All @@ -165,15 +170,19 @@ export const CodeBlockHighlight: ExtensionAuto<CodeBlockHighlightOptions> = (bui
const newLang = newNode.attrs[CodeBlockNodeAttr.Lang];
if (prevLang !== newLang) {
contentDOM.className = 'hljs';
updateDomAttribute(dom, CodeBlockNodeAttr.Lang, newLang);
if (newLang) {
dom.setAttribute(CodeBlockNodeAttr.Lang, newLang);
contentDOM.classList.add(newLang);
} else {
dom.removeAttribute(CodeBlockNodeAttr.Lang);
}
prevLang = newLang;
}

updateDomAttribute(
dom,
CodeBlockNodeAttr.Line,
newNode.attrs[CodeBlockNodeAttr.Line],
);

return true;
},
};
Expand Down Expand Up @@ -242,3 +251,11 @@ function stepHasFromTo(step: Step): step is Step & {from: number; to: number} {
// @ts-expect-error
return typeof step.from === 'number' && typeof step.to === 'number';
}

function updateDomAttribute(elem: Element, attr: string, value: string | null | undefined) {
if (value) {
elem.setAttribute(attr, value);
} else {
elem.removeAttribute(attr);
}
}
10 changes: 9 additions & 1 deletion src/extensions/markdown/CodeBlock/CodeBlockSpecs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {nodeTypeFactory} from '../../../../utils/schema';
export const CodeBlockNodeAttr = {
Lang: 'data-language',
Markup: 'data-markup',
Line: 'data-line',
} as const;

export const codeBlockNodeName = 'code_block';
Expand Down Expand Up @@ -38,6 +39,7 @@ export const CodeBlockSpecs: ExtensionAuto<CodeBlockSpecsOptions> = (builder, op
attrs: {
[CodeBlockNodeAttr.Lang]: {default: ''},
[CodeBlockNodeAttr.Markup]: {default: '```'},
[CodeBlockNodeAttr.Line]: {default: null},
},
content: 'text*',
group: 'block',
Expand Down Expand Up @@ -65,6 +67,11 @@ export const CodeBlockSpecs: ExtensionAuto<CodeBlockSpecsOptions> = (builder, op
name: codeBlockNodeName,
type: 'block',
noCloseToken: true,
getAttrs: (tok) => {
return {
[CodeBlockNodeAttr.Line]: tok.attrGet('data-line'),
};
},
prepareContent: removeNewLineAtEnd, // content of code blocks contains extra \n at the end
},
},
Expand All @@ -90,8 +97,9 @@ export const CodeBlockSpecs: ExtensionAuto<CodeBlockSpecsOptions> = (builder, op
type: 'block',
noCloseToken: true,
getAttrs: (tok) => {
const attrs: Record<string, string> = {
const attrs: Record<string, string | null> = {
[CodeBlockNodeAttr.Markup]: tok.markup,
[CodeBlockNodeAttr.Line]: tok.attrGet('data-line'),
};
if (tok.info) {
// like in markdown-it
Expand Down
4 changes: 4 additions & 0 deletions src/extensions/markdown/Deflist/DeflistSpecs/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ export enum DeflistNode {
Term = 'dt',
Desc = 'dd',
}

export const DeflistAttrs = {
DataLine: 'data-line',
};
12 changes: 10 additions & 2 deletions src/extensions/markdown/Deflist/DeflistSpecs/parser.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import type {ParserToken} from '../../../../core';

import {DeflistNode} from './const';
import {DeflistAttrs, DeflistNode} from './const';

export const parserTokens: Record<DeflistNode, ParserToken> = {
[DeflistNode.List]: {name: DeflistNode.List, type: 'block'},

[DeflistNode.Term]: {name: DeflistNode.Term, type: 'block'},
[DeflistNode.Term]: {
name: DeflistNode.Term,
type: 'block',
getAttrs(token) {
return {
[DeflistAttrs.DataLine]: token.attrGet('data-line'),
};
},
},

[DeflistNode.Desc]: {name: DeflistNode.Desc, type: 'block'},
};
7 changes: 4 additions & 3 deletions src/extensions/markdown/Deflist/DeflistSpecs/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type {NodeSpec} from 'prosemirror-model';

import type {PlaceholderOptions} from '../../../../utils/placeholder';

import {DeflistNode} from './const';
import {DeflistAttrs, DeflistNode} from './const';

import type {DeflistSpecsOptions} from './index';

Expand All @@ -28,12 +28,13 @@ export const getSchemaSpecs = (
},

[DeflistNode.Term]: {
attrs: {[DeflistAttrs.DataLine]: {default: null}},
defining: true,
group: 'block',
content: 'inline*',
parseDOM: [{tag: 'dt'}],
toDOM() {
return ['dt', 0];
toDOM(node) {
return ['dt', node.attrs, 0];
},
placeholder: {
content:
Expand Down
1 change: 1 addition & 0 deletions src/extensions/markdown/Lists/ListsSpecs/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export enum ListsAttr {
/** used in ordered list only */
Order = 'order',
Markup = 'markup',
DataLine = 'data-line',
}

export const Markup = {
Expand Down
5 changes: 4 additions & 1 deletion src/extensions/markdown/Lists/ListsSpecs/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ export const parserTokens: Record<ListNode, ParserToken> = {
[ListNode.ListItem]: {
name: ListNode.ListItem,
type: 'block',
getAttrs: (token) => ({[ListsAttr.Markup]: token.markup}),
getAttrs: (token) => ({
[ListsAttr.Markup]: token.markup,
[ListsAttr.DataLine]: token.attrGet('data-line'),
}),
},

[ListNode.BulletList]: {
Expand Down
5 changes: 3 additions & 2 deletions src/extensions/markdown/Lists/ListsSpecs/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ export const schemaSpecs: Record<ListNode, NodeSpec> = {
[ListNode.ListItem]: {
attrs: {
[ListsAttr.Markup]: {default: null},
[ListsAttr.DataLine]: {default: null},
},
content: '(paragraph|block)+',
defining: true,
parseDOM: [{tag: 'li'}],
toDOM() {
return ['li', 0];
toDOM(node) {
return ['li', node.attrs, 0];
},
selectable: true,
allowSelection: false,
Expand Down
1 change: 1 addition & 0 deletions src/extensions/markdown/Table/TableSpecs/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export enum TableNode {

export enum TableAttrs {
CellAlign = 'cell-align',
DataLine = 'data-line',
}

export enum CellAlign {
Expand Down
10 changes: 9 additions & 1 deletion src/extensions/markdown/Table/TableSpecs/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@ export const parserTokens: Record<TableNode, ParserToken> = {

[TableNode.Body]: {name: TableNode.Body, type: 'block'},

[TableNode.Row]: {name: TableNode.Row, type: 'block'},
[TableNode.Row]: {
name: TableNode.Row,
type: 'block',
getAttrs(token) {
return {
[TableAttrs.DataLine]: token.attrGet('data-line'),
};
},
},

[TableNode.HeaderCell]: {
name: TableNode.HeaderCell,
Expand Down
5 changes: 3 additions & 2 deletions src/extensions/markdown/Table/TableSpecs/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,12 @@ export const schemaSpecs: Record<TableNode, NodeSpec> = {

[TableNode.Row]: {
group: 'block',
attrs: {[TableAttrs.DataLine]: {default: null}},
content: `(${TableNode.HeaderCell}|${TableNode.DataCell})+`,
isolating: true,
parseDOM: [{tag: 'tr'}],
toDOM() {
return ['tr', 0];
toDOM(node) {
return ['tr', node.attrs, 0];
},
tableRole: TableRole.Row,
selectable: false,
Expand Down
1 change: 1 addition & 0 deletions src/extensions/yfm/Checkbox/CheckboxSpecs/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const CheckboxAttr = {
Id: 'id',
Checked: 'checked',
For: 'for',
DataLine: 'data-line',
} as const;

export const idPrefix = 'yfm-editor-checkbox';
Expand Down
1 change: 1 addition & 0 deletions src/extensions/yfm/Checkbox/CheckboxSpecs/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const getSchemaSpecs = (
allowSelection: false,
attrs: {
[CheckboxAttr.Class]: {default: b()},
[CheckboxAttr.DataLine]: {default: null},
},
parseDOM: [
{
Expand Down
1 change: 1 addition & 0 deletions src/extensions/yfm/YfmCut/YfmCutSpecs/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export enum CutNode {
export enum CutAttr {
Class = 'class',
Markup = 'data-markup',
DataLine = 'data-line',
}

export const cutType = nodeTypeFactory(CutNode.Cut);
Expand Down
15 changes: 14 additions & 1 deletion src/extensions/yfm/YfmCut/YfmCutSpecs/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ const getAttrs: ParserToken['getAttrs'] = (tok) => {

export const parserTokens: Record<CutNode, ParserToken> = {
[CutNode.Cut]: {name: CutNode.Cut, type: 'block', getAttrs},
[CutNode.CutTitle]: {name: CutNode.CutTitle, type: 'block'},
[CutNode.CutTitle]: {
name: CutNode.CutTitle,
type: 'block',
getAttrs: (token, tokens, index) => {
let dataLine = token.attrGet('data-line');
if (!dataLine) {
const prevToken = tokens[index - 1];
if (prevToken?.type === 'yfm_cut_open') {
dataLine = prevToken.attrGet('data-line');
}
}
return {[CutAttr.DataLine]: dataLine};
},
},
[CutNode.CutContent]: {name: CutNode.CutContent, type: 'block'},
};
Loading
Loading