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
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,11 @@
"description": "%print.markdown.watermark.enable%",
"default": false
},
"print.markdown.showFrontmatter": {
"type": "boolean",
"description": "%print.markdown.showFrontmatter%",
"default": true
},
"print.plaintext.stylesheets": {
"type": "array",
"items": {
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"print.markdown.stylesheets": "URLs or paths for CSS for rendered Markdown. For workspace relative paths, use 'workspace.resource/path/to/your.css'. For absolute filesystem paths, use 'absolute/path/to/your.css'. Otherwise, paths are relative to the base document ('./my.css' would be in the same folder as the document), or absolute URLs (https://...). For folders or multi-file selections use absolute, absolute filesystem paths, or workspace-relative paths.",
"print.markdown.smartQuotes.enable": "Enable the use of smart quotes in rendered Markdown.",
"print.markdown.watermark.enable": "Enable embedded watermark text",
"print.markdown.showFrontmatter": "Show frontmatter metadata as a table at the top of printed documents",
"print.markdown.watermark.text": "Watermark text to be periodically embedded in the rendered document",
"print.plaintext.stylesheets": "URLs or paths for extra CSS to be applied to plaintext. For workspace relative paths, use 'workspace.resource/path/to/your.css'. For absolute filesystem paths, use 'absolute/path/to/your.css'. Otherwise, paths are relative to the base document ('./my.css' would be in the same folder as the document), or absolute URLs (https://...). For folders or multi-file selections use absolute, absolute filesystem paths, or workspace-relative paths.",
"print.sourcecode.colourScheme.markdownDescription": "[Stylesheet preview is available online](https://highlightjs.org/demo/). Only 'light' versions are offered because paper is white.",
Expand Down
116 changes: 116 additions & 0 deletions src/renderers/frontmatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import * as yaml from "yaml";
import { logger } from '../logger';

/**
* Represents the result of extracting frontmatter from markdown content
*/
export interface FrontmatterResult {
frontmatter: any | null;
content: string;
}

/**
* HTML escaping utility function for frontmatter values
*/
function escapeHtml(text: string): string {
return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

/**
* Extracts frontmatter from markdown content
* Supports both YAML (---) and TOML (+++) delimiters
*
* @param raw The raw markdown content
* @returns Object containing parsed frontmatter and remaining content
*/
export function extractFrontmatter(raw: string): FrontmatterResult {
// Check if content starts with frontmatter delimiters (--- or +++)
const yamlDelimiterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n/;
const tomlDelimiterRegex = /^\+\+\+\s*\n([\s\S]*?)\n\+\+\+\s*\n/;

let frontmatterMatch = raw.match(yamlDelimiterRegex);
let frontmatterContent = '';
let isToml = false;

if (!frontmatterMatch) {
frontmatterMatch = raw.match(tomlDelimiterRegex);
isToml = true;
}

if (frontmatterMatch) {
frontmatterContent = frontmatterMatch[1];
const content = raw.substring(frontmatterMatch[0].length);

try {
let frontmatterData;
if (isToml) {
// For TOML, we'll try to parse it as YAML for now
// You might want to add a TOML parser like @iarna/toml if needed
frontmatterData = yaml.parse(frontmatterContent);
} else {
frontmatterData = yaml.parse(frontmatterContent);
}

return { frontmatter: frontmatterData, content };
} catch (error) {
logger.warn(`Failed to parse frontmatter: ${error}`);
return { frontmatter: null, content: raw };
}
}

return { frontmatter: null, content: raw };
}

/**
* Converts frontmatter object to HTML table string
*
* @param frontmatter The parsed frontmatter object
* @returns HTML string representing the frontmatter as a table
*/
export function frontmatterToTable(frontmatter: any): string {
if (!frontmatter || typeof frontmatter !== 'object') {
return '';
}

const rows = Object.entries(frontmatter).map(([key, value]) => {
let displayValue = '';
if (value === null || value === undefined) {
displayValue = '';
} else if (Array.isArray(value)) {
displayValue = value.join(', ');
} else if (typeof value === 'object') {
displayValue = JSON.stringify(value, null, 2);
} else {
displayValue = String(value);
}

return ` <tr>
<th class="frontmatter-key">${escapeHtml(key)}</th>
<td class="frontmatter-value">${escapeHtml(displayValue)}</td>
</tr>`;
}).join('\n');

if (rows) {
return `<div class="frontmatter-metadata">
<h3 class="frontmatter-title">Document Metadata</h3>
<table class="frontmatter-table">
${rows}
</table>
</div>`;
}

return '';
}

/**
* Checks if the given string has frontmatter delimiters at the start
*
* @param raw The raw markdown content
* @returns true if frontmatter is detected, false otherwise
*/
export function hasFrontmatter(raw: string): boolean {
const yamlDelimiterRegex = /^---\s*\n/;
const tomlDelimiterRegex = /^\+\+\+\s*\n/;

return yamlDelimiterRegex.test(raw) || tomlDelimiterRegex.test(raw);
}
23 changes: 22 additions & 1 deletion src/renderers/processMarkdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import * as fs from "fs";
import { resolveRootDoc } from './includes';
import { databaseFencedLanguageService } from "./database-fenced-language/databaseFencedLanguageService";
import { DatabaseDiagramRequest } from "./database-fenced-language/models/request/databaseDiagramRequest";
import { extractFrontmatter, frontmatterToTable } from './frontmatter';

const HIGHLIGHTJS_LANGS = hljs.listLanguages().map(s => s.toUpperCase());
const KROKI_SUPPORT = [
Expand All @@ -37,7 +38,10 @@ function escapeHtml(text: string): string {

export async function processFencedBlocks(defaultConfig: any, raw: string, generatedResources: Map<string, ResourceProxy>, rootDocFolder: string) {

const tokens = marked.lexer(raw);
// Extract frontmatter first
const { frontmatter, content } = extractFrontmatter(raw);

const tokens = marked.lexer(content);
const krokiUrl = vscode.workspace.getConfiguration("print.markdown.kroki").url;
let activeConfigName = "DEFAULT";
const namedConfigs: any = { DEFAULT: defaultConfig };
Expand Down Expand Up @@ -224,6 +228,23 @@ export async function processFencedBlocks(defaultConfig: any, raw: string, gener
updatedTokens.push(token);
};
}

const showFrontmatter = vscode.workspace.getConfiguration("print.markdown").get<boolean>("showFrontmatter", true);

// If frontmatter exists, add it as an HTML table at the beginning
if (showFrontmatter && frontmatter) {
const frontmatterTableHtml = frontmatterToTable(frontmatter);
if (frontmatterTableHtml) {
const frontmatterToken: Token = {
type: 'html',
raw: '',
text: frontmatterTableHtml,
block: true
};
updatedTokens.unshift(frontmatterToken);
}
}

return updatedTokens;
}

Expand Down