Skip to content

Commit

Permalink
Add heading class to the entire line
Browse files Browse the repository at this point in the history
The heading slug state field now includes Setext headings and
ATX headings alike and now handles marks using the HeaderMark
node in the AST. Updated tests to conform with behaviour.
  • Loading branch information
retronav committed Jun 23, 2022
1 parent d95997c commit 70da999
Show file tree
Hide file tree
Showing 4 changed files with 35 additions and 52 deletions.
28 changes: 13 additions & 15 deletions src/plugins/heading.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,23 +89,21 @@ class HeadingDecorationsPlugin {
private decorateHeadings(view: EditorView) {
const widgets = [];
iterateTreeInVisibleRanges(view, {
enter: ({ type, from, to }) => {
if (!type.name.includes('Heading')) return;
enter: ({ name, from }) => {
// To capture ATXHeading and SetextHeading
if (!name.includes('Heading')) return;
const slug = view.state
.field(headingSlugField)
.find((s) => s.pos === from)?.slug;
const createDec = (level: number) =>
Decoration.mark({
tagName: 'span',
class: [
'cm-heading',
`cm-heading-${level}`,
slug ? `cm-heading-slug-${slug}` : ''
].join(' ')
});
const level = parseInt(/[1-6]/.exec(type.name)[0]);
const dec = createDec(level);
widgets.push(dec.range(from, to));
.find((s) => s.pos === from).slug;
const level = parseInt(/[1-6]$/.exec(name)[0]);
const dec = Decoration.line({
class: [
'cm-heading',
`cm-heading-${level}`,
`cm-heading-slug-${slug}`
].join(' ')
});
widgets.push(dec.range(view.state.doc.lineAt(from).from));
}
});
return Decoration.set(widgets, true);
Expand Down
1 change: 1 addition & 0 deletions src/plugins/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ function getLinkAnchor(view: EditorView) {
enter: ({ type, from, to, node }) => {
if (type.name !== 'URL') return;
const parent = node.parent;
// FIXME: make this configurable
const blackListedParents = ['Image'];
if (parent && !blackListedParents.includes(parent.name)) {
const marks = parent.getChildren('LinkMark');
Expand Down
30 changes: 12 additions & 18 deletions src/state/heading-slug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ export interface HeadingSlug {
pos: number;
}

// A header starts with a hash character (upto 6), then a space,
// then the header content.
const headingStartRE = /^(#{1,6}\s)/;

/**
* A plugin that stores the calculated slugs of the document headings in the
* editor state. These can be useful when resolving links to headings inside
Expand All @@ -27,11 +23,9 @@ export const headingSlugField = StateField.define<HeadingSlug[]>({
extractSlugs(state);
return slugs;
},
update: (_value, tx) => {
// It seems very hard to incrementially calculate slugs, so a
// recalculation is the best option.
const slugs = extractSlugs(tx.state);
return slugs;
update: (value, tx) => {
if (tx.docChanged) return extractSlugs(tx.state);
return value;
},
compare: (a, b) =>
a.length === b.length &&
Expand All @@ -47,15 +41,15 @@ function extractSlugs(state: EditorState): HeadingSlug[] {
const slugs: HeadingSlug[] = [];
const slugger = new Slugger();
syntaxTree(state).iterate({
enter: ({ type, from, to }) => {
if (!type.name.includes('ATXHeading')) return;
const slug = slugger.slug(
// TODO: There can be areas if the heading has
// marks and could result in weird behaviour,
// this should be investigated.
state.sliceDoc(from, to).replace(headingStartRE, '')
);
if (slug) slugs.push({ slug, pos: from });
enter: ({ name, from, to, node }) => {
// Capture ATXHeading and SetextHeading
if (!name.includes('Heading')) return;
const mark = node.getChild('HeaderMark');

const headerText = state.sliceDoc(from, to).split('');
headerText.splice(mark.from - from, mark.to - mark.from);
const slug = slugger.slug(headerText.join('').trim());
slugs.push({ slug, pos: from });
}
});
return slugs;
Expand Down
28 changes: 9 additions & 19 deletions test/heading.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('Heading plugin', () => {
).to.equal(headingContent);

// Move the cursor to a position after the heading
moveCursor("line", 1, editor);
moveCursor('line', 1, editor);

// CodeMirror uses this to mark the positions of hidden widgets
expect(
Expand All @@ -49,25 +49,20 @@ describe('Heading plugin', () => {

it('Should add an appropriate slug to heading', () => {
const headingEl = editor.domAtPos(0).node as HTMLElement;
expect(headingEl.firstElementChild).to.exist;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expect(Array.from(headingEl.firstElementChild!.classList)).to.contain(
'cm-heading-slug-hello'
);
expect(headingEl).to.have.class('cm-heading-slug-hello');

const pos = editor.viewportLineBlocks[2].from;
const thirdHeadingEl = editor.domAtPos(pos).node as HTMLElement;
expect(thirdHeadingEl.querySelector('.cm-heading-slug-hello-2')).to
.exist;
expect(thirdHeadingEl).to.have.class('cm-heading-slug-hello-2');
});

it('Should add a class with heading level', () => {
const headingEl = editor.domAtPos(0).node as HTMLElement;
const heading2El = editor.domAtPos(editor.viewportLineBlocks[1].from)
.node as HTMLElement;

expect(headingEl.querySelector('.cm-heading-1')).to.exist;
expect(heading2El.querySelector('.cm-heading-2')).to.exist;
expect(headingEl).to.have.class('cm-heading-1');
expect(heading2El).to.have.class('cm-heading-2');
});

it('Should support Setext headings and not hide the underline', () => {
Expand All @@ -77,14 +72,9 @@ describe('Heading plugin', () => {
setEditorContent(content, editor);

const headingEl = editor.domAtPos(0).node as HTMLElement;
const headingLineEl = editor.domAtPos(editor.viewportLineBlocks[1].from)
.node as HTMLElement;

expect(headingEl.querySelector('.cm-heading-1')).to.exist.and.have.text(
content.split('\n')[0]
);
expect(
headingLineEl.querySelector('.cm-heading-1')
).to.exist.and.have.text(content.split('\n')[1]);
expect(headingEl)
.to.exist.and.have.class('cm-heading-1')
// .and.have.class('cm-heading-slug-hello')
.and.have.text(content.split('\n')[0]);
});
});

0 comments on commit 70da999

Please sign in to comment.