Skip to content
Open
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
44 changes: 44 additions & 0 deletions extensions/markdown-language-features/media/markdown.css
Original file line number Diff line number Diff line change
Expand Up @@ -453,3 +453,47 @@ pre {
.vscode-dark td {
border-color: rgba(255, 255, 255, 0.18);
}

/* Front matter rendering */
table.frontmatter {
margin-bottom: 16px;
border-collapse: collapse;
}

table.frontmatter th,
table.frontmatter td {
padding: 6px 13px;
border: 1px solid var(--vscode-widget-border, rgba(127, 127, 127, 0.35));
text-align: left;
vertical-align: top;
}

table.frontmatter th {
font-weight: 600;
white-space: nowrap;
}

table.frontmatter td > ul {
margin: 0;
padding-left: 1.2em;
}

pre.frontmatter {
margin-bottom: 16px;
}

.frontmatter-error {
margin-bottom: 16px;
padding: 8px 13px;
border-left: 4px solid var(--vscode-editorError-foreground, #f48771);
background: var(--vscode-inputValidation-errorBackground, rgba(244, 135, 113, 0.1));
color: var(--vscode-editorError-foreground, #f48771);
}

.frontmatter-error pre {
margin: 6px 0 0;
white-space: pre-wrap;
color: inherit;
background: transparent;
padding: 0;
}
24 changes: 17 additions & 7 deletions extensions/markdown-language-features/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 18 additions & 2 deletions extensions/markdown-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,22 @@
"%configuration.markdown.preview.openMarkdownLinks.inPreview%",
"%configuration.markdown.preview.openMarkdownLinks.inEditor%"
]
},
"markdown.preview.frontMatter": {
"type": "string",
"default": "table",
"scope": "resource",
"markdownDescription": "%configuration.markdown.preview.frontMatter.description%",
"enum": [
"hide",
"codeBlock",
"table"
],
"enumDescriptions": [
"%configuration.markdown.preview.frontMatter.hide%",
"%configuration.markdown.preview.frontMatter.codeBlock%",
"%configuration.markdown.preview.frontMatter.table%"
]
}
}
},
Expand Down Expand Up @@ -860,14 +876,14 @@
"dompurify": "^3.4.1",
"highlight.js": "^11.8.0",
"markdown-it": "^12.3.2",
"markdown-it-front-matter": "^0.2.4",
"morphdom": "^2.7.7",
"picomatch": "^2.3.2",
"punycode": "^2.3.1",
"vscode-languageclient": "^8.0.2",
"vscode-languageserver-textdocument": "^1.0.11",
"vscode-markdown-languageserver": "0.5.0-alpha.15",
"vscode-uri": "^3.0.3"
"vscode-uri": "^3.0.3",
"yaml": "^2.8.3"
},
"devDependencies": {
"@types/dompurify": "^3.0.5",
Expand Down
4 changes: 4 additions & 0 deletions extensions/markdown-language-features/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
"configuration.markdown.preview.openMarkdownLinks.description": "Controls how links to other Markdown files in the Markdown preview should be opened.",
"configuration.markdown.preview.openMarkdownLinks.inEditor": "Try to open links in the editor.",
"configuration.markdown.preview.openMarkdownLinks.inPreview": "Try to open links in the Markdown preview.",
"configuration.markdown.preview.frontMatter.description": "Controls how YAML front matter (delimited by `---`) at the start of a Markdown file is rendered in the preview.",
"configuration.markdown.preview.frontMatter.hide": "Do not render front matter.",
"configuration.markdown.preview.frontMatter.codeBlock": "Render front matter as a code block.",
"configuration.markdown.preview.frontMatter.table": "Render front matter as a table of keys and values.",
"configuration.markdown.links.openLocation.description": "Controls where links in Markdown files should be opened.",
"configuration.markdown.links.openLocation.currentGroup": "Open links in the active editor group.",
"configuration.markdown.links.openLocation.beside": "Open links beside the active editor.",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type MarkdownIt from 'markdown-it';
import type Token from 'markdown-it/lib/token.mjs';
import * as vscode from 'vscode';
import * as yaml from 'yaml';
import { escapeHtml } from '../../util/dom';
Comment on lines +6 to +10

export type FrontMatterRenderStyle = 'hide' | 'codeBlock' | 'table';

const FRONT_MATTER_TOKEN = 'front_matter';
const MARKER = '---';

interface IFrontMatterMeta {
readonly content: string;
}

/**
* Extends a `markdown-it` instance with parsing and rendering support for YAML
* front matter at the start of a Markdown document.
*
* Front matter is delimited by lines containing only `---`. How (or whether) the parsed
* front matter is rendered in the preview is controlled by the `markdown.preview.frontMatter`
* setting.
*/
export function extendMarkdownIt(md: MarkdownIt): MarkdownIt {
md.block.ruler.before('fence', FRONT_MATTER_TOKEN, frontMatterRule, {
alt: ['paragraph', 'reference', 'blockquote', 'list']
});

md.renderer.rules[FRONT_MATTER_TOKEN] = renderFrontMatter;

return md;
}

const frontMatterRule = (state: MarkdownIt.StateBlock, startLine: number, endLine: number, silent: boolean): boolean => {
if (startLine !== 0 || state.tShift[startLine] !== 0) {
return false;
}

const firstLineStart = state.bMarks[startLine];
const firstLineEnd = state.eMarks[startLine];
const firstLine = state.src.slice(firstLineStart, firstLineEnd).replace(/\s+$/, '');

if (firstLine !== MARKER) {
return false;
}

let nextLine = startLine + 1;
let foundEnd = false;
for (; nextLine < endLine; nextLine++) {
if (state.tShift[nextLine] !== 0) {
continue;
}
const lineStart = state.bMarks[nextLine];
const lineEnd = state.eMarks[nextLine];
const line = state.src.slice(lineStart, lineEnd).replace(/\s+$/, '');
if (line === MARKER) {
foundEnd = true;
break;
}
}

if (!foundEnd) {
return false;
}

if (silent) {
return true;
}

const contentStart = state.bMarks[startLine + 1];
const contentEnd = state.bMarks[nextLine];
const rawContent = state.src.slice(contentStart, contentEnd).replace(/\n$/, '');

const token = state.push(FRONT_MATTER_TOKEN, '', 0);
token.block = true;
token.hidden = false;
token.markup = MARKER;
token.map = [startLine, nextLine + 1];
const meta: IFrontMatterMeta = { content: rawContent };
token.meta = meta;

state.line = nextLine + 1;
return true;
};

function renderFrontMatter(tokens: Token[], idx: number, options: MarkdownIt.Options, env: unknown): string {
const meta = tokens[idx].meta as IFrontMatterMeta | undefined;
if (!meta) {
return '';
}

const currentDocument = (env as { currentDocument?: vscode.Uri } | undefined)?.currentDocument;
const style = getFrontMatterRenderStyle(currentDocument);

switch (style) {
case 'codeBlock':
return renderAsCodeBlock(meta, options);
case 'table':
return renderAsTable(meta);
case 'hide':
default:
return '';
}
}

function getFrontMatterRenderStyle(resource: vscode.Uri | undefined): FrontMatterRenderStyle {
const config = vscode.workspace.getConfiguration('markdown', resource ?? null);
const value = config.get<string>('preview.frontMatter', 'table');
switch (value) {
case 'codeBlock':
case 'table':
case 'hide':
return value;
default:
return 'table';
}
}

function renderAsCodeBlock(meta: IFrontMatterMeta, options: MarkdownIt.Options): string {
let highlighted: string | undefined;
if (typeof options.highlight === 'function') {
try {
highlighted = options.highlight(meta.content, 'yaml', '') || undefined;
} catch {
highlighted = undefined;
}
}
if (highlighted?.startsWith('<pre')) {
return highlighted + '\n';
}
const body = highlighted ?? escapeHtml(meta.content);
return `<pre class="frontmatter hljs"><code class="language-yaml">${body}</code></pre>\n`;
}

function renderAsTable(meta: IFrontMatterMeta): string {
const result = parseEntries(meta);
if (result.error !== undefined) {
return renderError(result.error);
}
if (!result.entries.length) {
return '';
}
const rows = result.entries.map(([key, value]) =>
`<tr><th>${escapeHtml(key)}</th><td>${formatValueHtml(value)}</td></tr>`
).join('');
return `<table class="frontmatter"><tbody>${rows}</tbody></table>\n`;
}

function renderError(message: string): string {
const label = vscode.l10n.t('Failed to parse front matter');
return `<div class="frontmatter-error" role="alert"><strong>${escapeHtml(label)}</strong><pre>${escapeHtml(message)}</pre></div>\n`;
}

interface IParseResult {
readonly entries: readonly [string, unknown][];
readonly error?: string;
}

function parseEntries(meta: IFrontMatterMeta): IParseResult {
try {
const parsed = yaml.parse(meta.content);
if (parsed === null || parsed === undefined) {
return { entries: [] };
}
if (typeof parsed !== 'object' || Array.isArray(parsed)) {
return { entries: [['', parsed]] };
}
return { entries: Object.entries(parsed as Record<string, unknown>) };
} catch (e) {
return { entries: [], error: e instanceof Error ? e.message : String(e) };
}
}

function formatValueHtml(value: unknown): string {
if (value === null || value === undefined) {
return '';
}
if (Array.isArray(value)) {
if (!value.length) {
return '';
}
return `<ul>${value.map(v => `<li>${formatValueHtml(v)}</li>`).join('')}</ul>`;
}
if (typeof value === 'object') {
return `<code>${escapeHtml(yaml.stringify(value).trimEnd())}</code>`;
}
return escapeHtml(formatScalar(value));
}

function formatScalar(value: unknown): string {
if (value instanceof Date) {
return value.toISOString();
}
return String(value);
}
16 changes: 2 additions & 14 deletions extensions/markdown-language-features/src/markdownEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import type MarkdownIt from 'markdown-it';
import * as vscode from 'vscode';
import { extendMarkdownIt as extendMarkdownItWithFrontMatter } from './extensions/yamlPreamble/yamlPreamble';
import { ILogger } from './logging';
import { MarkdownContributionProvider } from './markdownExtensions';
import { MarkdownPreviewConfiguration } from './preview/previewConfig';
Expand Down Expand Up @@ -144,20 +145,7 @@ export class MarkdownItEngine implements IMdParser {
}
}

const frontMatterPlugin = await import('markdown-it-front-matter');
// Extract rules from front matter plugin and apply at a lower precedence
let fontMatterRule: any;
frontMatterPlugin.default({
block: {
ruler: {
before: (_id: any, _id2: any, rule: any) => { fontMatterRule = rule; }
}
}
}, () => { /* noop */ });

md.block.ruler.before('fence', 'front_matter', fontMatterRule, {
alt: ['paragraph', 'reference', 'blockquote', 'list']
});
md = extendMarkdownItWithFrontMatter(md);

this.#addImageRenderer(md);
this.#addFencedRenderer(md);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export class MarkdownPreviewConfiguration {
public readonly previewLineBreaks: boolean;
public readonly previewLinkify: boolean;
public readonly previewTypographer: boolean;
public readonly previewFrontMatter: string;

public readonly doubleClickToSwitchToEditor: boolean;
public readonly scrollEditorWithPreview: boolean;
Expand Down Expand Up @@ -46,6 +47,7 @@ export class MarkdownPreviewConfiguration {
this.previewLineBreaks = !!markdownConfig.get<boolean>('preview.breaks', false);
this.previewLinkify = !!markdownConfig.get<boolean>('preview.linkify', true);
this.previewTypographer = !!markdownConfig.get<boolean>('preview.typographer', false);
this.previewFrontMatter = markdownConfig.get<string>('preview.frontMatter', 'table');

this.doubleClickToSwitchToEditor = !!markdownConfig.get<boolean>('preview.doubleClickToSwitchToEditor', true);
this.markEditorSelection = !!markdownConfig.get<boolean>('preview.markEditorSelection', true);
Expand Down
Loading
Loading