Skip to content
Merged
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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ In [settings.json](https://github.com/MarkEdit-app/MarkEdit/wiki/Customization#a
"hidePreviewButtons": true,
"syntaxAutoDetect": false,
"imageHoverPreview": false,
"inlineImages": false,
"inlineRendering": ["table", "math", "mermaid"],
"themeName": "github",
"styledHtmlColorScheme": "auto",
"mathDelimiters": [],
Expand All @@ -67,7 +67,8 @@ In [settings.json](https://github.com/MarkEdit-app/MarkEdit/wiki/Customization#a
- `hidePreviewButtons`: Whether to hide the built-in preview buttons in side-by-side mode (not applicable for lite build).
- `syntaxAutoDetect`: Whether to enable automatic language detection for syntax highlighting in code blocks (not applicable for lite build).
- `imageHoverPreview`: Whether to enable image preview on hover.
- `inlineImages`: Whether Mixed mode replaces image links with inline images.
- `inlineImages` (deprecated): Whether Mixed mode renders images inline, defaults to `false`. Use `inlineRendering` instead; an explicit `inlineRendering` array takes precedence.
- `inlineRendering`: Types rendered inline in Mixed mode: `"image"`, `"table"`, `"math"`, and `"mermaid"`. Defaults to `["table", "math", "mermaid"]`; use `[]` to disable all four. Math and Mermaid rendering require the full build.
- `themeName`: Set the preview color theme, available themes can be found in the [`styles/themes`](styles/themes) folder. Use `"none"` to disable preview styling and render the raw HTML.
- `styledHtmlColorScheme`: Determine the color scheme of saving styled html files, valid values are `light`, `dark`, and `auto`.
- `mathDelimiters`: Customize math delimiters for KaTeX rendering (not applicable for lite build), each delimiter object has `left`, `right`, and `display` properties, defaults to `$...$`, `$$...$$`, `\(...\)`, and `\[...\]`.
Expand Down
132 changes: 66 additions & 66 deletions dist/lite/markedit-preview.js

Large diffs are not rendered by default.

614 changes: 307 additions & 307 deletions dist/markedit-preview.js

Large diffs are not rendered by default.

12 changes: 7 additions & 5 deletions src/hiddenSyntax/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { SyntaxNodeRef } from '@lezer/common';
import { BlockMathWidget } from './components/math';
import { MermaidWidget } from './components/mermaid';
import { TableWidget, tableRenderFailed } from './components/table';
import { inlineRenderingConfig } from './config';
import { selectionReveals } from './selection';
import { renderTableBlocks } from '../render';

Expand Down Expand Up @@ -59,6 +60,7 @@ function createBlockDecorationState(state: EditorState, previous = Decoration.no
function createBlockDecorations(state: EditorState, previous: DecorationSet) {
const ranges: Range<Decoration>[] = [];
const tree = syntaxTree(state);
const rendering = state.facet(inlineRenderingConfig);

const context: string[] = [];
tree.iterate({
Expand Down Expand Up @@ -86,7 +88,7 @@ function createBlockDecorations(state: EditorState, previous: DecorationSet) {
tree.iterate({
enter: node => {
let decoration: Range<Decoration> | undefined;
if (node.name === 'Table') {
if (node.name === 'Table' && rendering.includes('table')) {
for (let parent = node.node.parent; parent !== null; parent = parent.parent) {
if (parent.name !== 'Document') {
return false;
Expand All @@ -112,10 +114,10 @@ function createBlockDecorations(state: EditorState, previous: DecorationSet) {
block: true,
widget,
}).range(from, to);
} else if (__FULL_BUILD__) {
decoration = node.name === 'BlockMath'
? blockMathDecoration(node, state)
: mermaidDecoration(node, state);
} else if (__FULL_BUILD__ && node.name === 'BlockMath' && rendering.includes('math')) {
decoration = blockMathDecoration(node, state);
} else if (__FULL_BUILD__ && rendering.includes('mermaid')) {
decoration = mermaidDecoration(node, state);
}

if (decoration !== undefined) {
Expand Down
6 changes: 6 additions & 0 deletions src/hiddenSyntax/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Facet } from '@codemirror/state';
import { inlineRendering, type InlineRenderingType } from '../support/settings';

export const inlineRenderingConfig = Facet.define<readonly InlineRenderingType[], readonly InlineRenderingType[]>({
combine: values => values[values.length - 1] ?? inlineRendering,
});
12 changes: 6 additions & 6 deletions src/hiddenSyntax/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { syntaxTree } from '@codemirror/language';
import { Facet, type Range } from '@codemirror/state';
import { type Range } from '@codemirror/state';
import { Decoration, type DecorationSet, EditorView, ViewPlugin, type ViewUpdate } from '@codemirror/view';
import { blockquoteAlert, blockquoteSyntaxRange } from './blockquote';
import { BlockquoteAlertWidget } from './components/alert';
Expand All @@ -10,6 +10,7 @@ import { LinkIconWidget } from './components/icon';
import { FootnoteDefinitionSuffix } from './components/footnote';
import { InlineImageWidget } from './components/image';
import { renderedBlockDecorations } from './block';
import { inlineRenderingConfig } from './config';
import { fencedCodeDecorations } from './fencedCode';
import { atxHeadingSyntaxRange, setextHeadingSyntaxLine } from './heading';
import { horizontalRuleDecoration } from './horizontalRule';
Expand All @@ -18,7 +19,7 @@ import { footnoteReferences, footnoteDefinitionSyntax, linkSyntax, referenceDest
import { hiddenSyntaxTheme } from './theme';
import { unorderedListSyntax } from './unorderedList';
import { correctedLineUp, selectionReveals, stablePointerSelection } from './selection';
import { inlineImages } from '../support/settings';
import { inlineRendering } from '../support/settings';

const hiddenSyntax = Decoration.mark({ class: 'cm-md-syntaxHiddenSource' });
const hiddenQuoteSyntax = Decoration.mark({ class: 'cm-md-syntaxHiddenSource cm-md-syntaxHiddenQuoteMark' });
Expand All @@ -27,7 +28,6 @@ const hiddenTaskListSyntax = Decoration.mark({ class: 'cm-md-syntaxHiddenSource
const hiddenLine = Decoration.line({ class: 'cm-md-syntaxHiddenSetextUnderline' });
const hiddenLinkLabel = Decoration.mark({ class: 'cm-md-syntaxHiddenLinkLabel' });
const hiddenImageLabel = Decoration.mark({ class: 'cm-md-syntaxHiddenImageLabel' });
const inlineImagesConfig = Facet.define<boolean, boolean>({ combine: values => values[values.length - 1] ?? inlineImages });

const hiddenSyntaxBaseExtension = [
EditorView.editorAttributes.of({
Expand Down Expand Up @@ -61,8 +61,8 @@ const hiddenSyntaxBaseExtension = [
hiddenSyntaxTheme,
];

export function createHiddenSyntaxExtension(renderInlineImages = inlineImages) {
return [inlineImagesConfig.of(renderInlineImages), hiddenSyntaxBaseExtension];
export function createHiddenSyntaxExtension(rendering = inlineRendering) {
return [inlineRenderingConfig.of(rendering), hiddenSyntaxBaseExtension];
}

export const hiddenSyntaxExtension = createHiddenSyntaxExtension();
Expand All @@ -71,7 +71,7 @@ function hiddenSyntaxDecorations(view: EditorView) {
const ranges: Range<Decoration>[] = [];
const alertMarkers = new Set<number>();
const codeBlocks = new Set<number>();
const renderInlineImages = view.state.facet(inlineImagesConfig);
const renderInlineImages = view.state.facet(inlineRenderingConfig).includes('image');
const resolveReferenceDestination = referenceDestinationResolver(view.state);

for (const { from, to } of view.visibleRanges) {
Expand Down
11 changes: 11 additions & 0 deletions src/support/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { JSONObject, JSONValue } from 'markedit-api';
import type { PresetName } from 'markdown-it';
import type { ColorScheme } from '../shared/types';

export type InlineRenderingType = 'image' | 'table' | 'math' | 'mermaid';

const Constants = {
rootValueKey: 'extension.markeditPreview',
defaultModes: ['edit', 'side-by-side', 'preview', 'syntax-hidden'],
Expand All @@ -19,7 +21,16 @@ export const syncScroll = toBoolean(rootValue.syncScroll);
export const hidePreviewButtons = toBoolean(rootValue.hidePreviewButtons);
export const syntaxAutoDetect = toBoolean(rootValue.syntaxAutoDetect, false);
export const imageHoverPreview = toBoolean(rootValue.imageHoverPreview, false);

/**
* @deprecated Use inlineRendering instead.
*/
export const inlineImages = toBoolean(rootValue.inlineImages, false);
export const inlineRendering: readonly InlineRenderingType[] = Array.isArray(rootValue.inlineRendering)
? rootValue.inlineRendering.filter((value): value is InlineRenderingType =>
value === 'image' || value === 'table' || value === 'math' || value === 'mermaid')
: inlineImages ? ['image', 'table', 'math', 'mermaid'] : ['table', 'math', 'mermaid'];

export const themeName = (rootValue.themeName ?? 'github') as string;
export const showRawHtml = themeName === 'none';
export const styledHtmlColorScheme = (rootValue.styledHtmlColorScheme ?? rootValue.styledHtmlTheme ?? 'auto') as ColorScheme; // styledHtmlTheme for backward compatibility
Expand Down
66 changes: 61 additions & 5 deletions tests/hiddenSyntax/block.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,69 @@
import { mermaidMocks, hiddenTexts, editorText } from './support';
import { describe, expect, test, vi } from 'vitest';
import { codeFolding, foldable, foldEffect, unfoldEffect } from '@codemirror/language';
import { Compartment } from '@codemirror/state';
import type { EditorView } from '@codemirror/view';
import { createHiddenSyntaxExtension, hiddenSyntaxExtension } from '../../src/hiddenSyntax';
import { BlockMathWidget } from '../../src/hiddenSyntax/components/math';
import { MermaidWidget } from '../../src/hiddenSyntax/components/mermaid';
import { renderMermaidSVG } from '../../src/render';
import type { InlineRenderingType } from '../../src/support/settings';
import * as editor from '../support/editor';

describe('Inline rendering options', () => {
const source = '![Alt](image.png)\n\n| Name |\n| --- |\n| Value |\n\n$$x = 1$$\n\n```mermaid\ngraph TD\n```\n\nAfter';
const selectors = {
image: '.cm-md-syntaxHiddenImage',
table: '.cm-md-syntaxHiddenTable',
math: '.cm-md-syntaxHiddenBlockMath',
mermaid: '.cm-md-syntaxHiddenMermaid',
};

const options: (readonly InlineRenderingType[] | undefined)[] = [
undefined, [], ['image'], ['table'], ['math'], ['mermaid'], ['image', 'table', 'math', 'mermaid'],
];

test.each(options.map(rendering => ({ rendering })))('renders only the configured types: $rendering', ({ rendering }) => {
editor.setUp(source, createHiddenSyntaxExtension(rendering));
window.editor.dispatch({ selection: { anchor: source.length } });
const enabled = rendering ?? ['table', 'math', 'mermaid'];
for (const type of Object.keys(selectors) as InlineRenderingType[]) {
expect(window.editor.dom.querySelector(selectors[type]) !== null).toBe(enabled.includes(type));
}

expect(window.editor.state.doc.toString()).toBe(source);
});

test('updates every type when the option set is reconfigured', () => {
const configuration = new Compartment();
editor.setUp(source, configuration.of(createHiddenSyntaxExtension([])));
window.editor.dispatch({ selection: { anchor: source.length } });

for (const rendering of [['image', 'table', 'math', 'mermaid'], []] as InlineRenderingType[][]) {
window.editor.dispatch({ effects: configuration.reconfigure(createHiddenSyntaxExtension(rendering)) });
for (const selector of Object.values(selectors)) {
expect(window.editor.dom.querySelector(selector) !== null).toBe(rendering.length > 0);
}
}

expect(window.editor.state.doc.toString()).toBe(source);
});

test('renders only images and tables in lite mode even when all types are enabled', () => {
vi.stubGlobal('__FULL_BUILD__', false);
try {
editor.setUp(source, createHiddenSyntaxExtension(['image', 'table', 'math', 'mermaid']));
window.editor.dispatch({ selection: { anchor: source.length } });
for (const type of Object.keys(selectors) as InlineRenderingType[]) {
expect(window.editor.dom.querySelector(selectors[type]) !== null).toBe(type === 'image' || type === 'table');
}
expect(window.editor.state.doc.toString()).toBe(source);
} finally {
vi.unstubAllGlobals();
}
});
});

test('keeps HTML source visible', () => {
const source = '<b>inline</b>\n\n<div>block</div>\n\nAfter';
editor.setUp(source, hiddenSyntaxExtension);
Expand Down Expand Up @@ -113,7 +169,7 @@ describe('Fenced code', () => {
describe('Inline images', () => {
test('renders remote images in place of their Markdown source', () => {
const source = 'Before ![Alt](https://example.com/image.png) after';
editor.setUp(source, createHiddenSyntaxExtension(true));
editor.setUp(source, createHiddenSyntaxExtension(['image']));
window.editor.dispatch({ selection: { anchor: source.length } });

const image = window.editor.dom.querySelector<HTMLImageElement>('.cm-md-syntaxHiddenImage');
Expand All @@ -126,7 +182,7 @@ describe('Inline images', () => {

test('loads local images through the image loader', () => {
const source = '![Local](images/photo.png) after';
editor.setUp(source, createHiddenSyntaxExtension(true));
editor.setUp(source, createHiddenSyntaxExtension(['image']));
window.editor.dispatch({ selection: { anchor: source.length } });

const image = window.editor.dom.querySelector<HTMLImageElement>('.cm-md-syntaxHiddenImage');
Expand All @@ -135,7 +191,7 @@ describe('Inline images', () => {

test('renders reference images with their resolved destination', () => {
const source = '![Photo][image]\n\n[image]: assets/photo.jpg';
editor.setUp(source, createHiddenSyntaxExtension(true));
editor.setUp(source, createHiddenSyntaxExtension(['image']));
window.editor.dispatch({ selection: { anchor: source.length } });

const image = window.editor.dom.querySelector<HTMLImageElement>('.cm-md-syntaxHiddenImage');
Expand All @@ -145,7 +201,7 @@ describe('Inline images', () => {

test('reveals image source when selected', () => {
const source = 'Before ![Alt](image.png) after';
editor.setUp(source, createHiddenSyntaxExtension(true));
editor.setUp(source, createHiddenSyntaxExtension(['image']));
window.editor.dispatch({ selection: { anchor: source.length } });
expect(window.editor.dom.querySelector('.cm-md-syntaxHiddenImage')).not.toBeNull();

Expand All @@ -156,7 +212,7 @@ describe('Inline images', () => {

test('keeps image source visible when inline images are disabled', () => {
const source = '![Alt](image.png) after';
editor.setUp(source, createHiddenSyntaxExtension(false));
editor.setUp(source, createHiddenSyntaxExtension([]));
window.editor.dispatch({ selection: { anchor: source.length } });

expect(window.editor.dom.querySelector('.cm-md-syntaxHiddenImage')).toBeNull();
Expand Down
2 changes: 1 addition & 1 deletion tests/hiddenSyntax/link.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ describe('Link syntax', () => {

test('hides image syntax when inline images are disabled', () => {
const source = 'Before ![alt](image.png) after';
editor.setUp(source, createHiddenSyntaxExtension(false));
editor.setUp(source, createHiddenSyntaxExtension([]));
window.editor.dispatch({ selection: { anchor: source.length } });

expect(editorText()).toBe('Before alt after');
Expand Down
46 changes: 46 additions & 0 deletions tests/settings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';

const settings = vi.hoisted<{ inlineRendering: unknown; inlineImages?: boolean }>(() => ({ inlineRendering: undefined }));
vi.mock('markedit-api', () => ({ MarkEdit: { userSettings: { 'extension.markeditPreview': settings } } }));
beforeEach(() => {
vi.resetModules();
settings.inlineRendering = undefined;
settings.inlineImages = undefined;
});

describe('Inline rendering settings', () => {
test.each([true, false])('preserves deprecated inlineImages: %s', async enabled => {
settings.inlineImages = enabled;
const { inlineRendering } = await import('../src/support/settings');
expect(inlineRendering).toEqual(enabled ? ['image', 'table', 'math', 'mermaid'] : ['table', 'math', 'mermaid']);
});

test.each([
{ legacy: true, rendering: [] },
{ legacy: true, rendering: ['table'] },
{ legacy: false, rendering: ['image'] },
])('prefers explicit inlineRendering: $rendering over inlineImages: $legacy', async ({ legacy, rendering }) => {
settings.inlineImages = legacy;
settings.inlineRendering = rendering;
const { inlineRendering } = await import('../src/support/settings');
expect(inlineRendering).toEqual(rendering);
});

test.each([undefined, null, false, 'image', {}])('uses defaults for a non-array value: %j', async value => {
settings.inlineRendering = value;
const { inlineRendering } = await import('../src/support/settings');
expect(inlineRendering).toEqual(['table', 'math', 'mermaid']);
});

test.each([
{ value: [], expected: [] },
{ value: ['image'], expected: ['image'] },
{ value: ['image', 'table', 'math', 'mermaid'], expected: ['image', 'table', 'math', 'mermaid'] },
{ value: ['unknown', null, 1, true, {}, 'table'], expected: ['table'] },
{ value: ['unknown'], expected: [] },
])('keeps only supported entries: $value', async ({ value, expected }) => {
settings.inlineRendering = value;
const { inlineRendering } = await import('../src/support/settings');
expect(inlineRendering).toEqual(expected);
});
});
Loading