Merge branch 'worktree-agent-aba1226f'

# Conflicts:
#	main.ts
#	src/settings/SettingsTab.ts
#	src/types/settings.ts
This commit is contained in:
Ethan Troy 2026-03-17 20:45:42 -04:00
commit bfdab4bec2
5 changed files with 219 additions and 4 deletions

14
main.ts
View file

@ -240,7 +240,7 @@ export default class MarkitdownPlugin extends Plugin {
inputPath
);
const outputPath = path.join(outputFolder, `${resolvedName}.md`);
const options = this.buildConversionOptions(outputPath);
const options = this.buildConversionOptions(outputPath, inputPath);
new Notice('Converting file...');
const result = await this.converter.convert(inputPath, outputPath, options);
@ -283,12 +283,12 @@ export default class MarkitdownPlugin extends Plugin {
inputPath: string,
outputPath: string
): Promise<ConversionResult> {
const options = this.buildConversionOptions(outputPath);
const options = this.buildConversionOptions(outputPath, inputPath);
return this.converter.convert(inputPath, outputPath, options);
}
/** Build ConversionOptions from current settings. */
buildConversionOptions(outputPath: string): ConversionOptions {
buildConversionOptions(outputPath: string, inputPath?: string): ConversionOptions {
const options: ConversionOptions = {
enablePlugins: this.settings.enablePlugins,
docintelEndpoint: this.settings.docintelEndpoint || undefined,
@ -306,6 +306,14 @@ export default class MarkitdownPlugin extends Plugin {
);
}
// Attach post-processing settings when hooks are enabled
if (inputPath && (this.settings.enableAutoFrontmatter || this.settings.autoTags.trim())) {
options.postProcess = {
settings: this.settings,
inputPath,
};
}
return options;
}

View file

@ -1,8 +1,9 @@
import * as path from 'path';
import * as fs from 'fs';
import { ConversionOptions, ConversionResult } from '../types/settings';
import { ConversionOptions, ConversionResult, MarkitdownSettings } from '../types/settings';
import { runPythonScript, getPythonScriptPath } from '../utils/python';
import { SUPPORTED_EXTENSIONS } from '../utils/fileTypes';
import { applyPostConversionHooks } from '../utils/postprocess';
export class MarkitdownConverter {
constructor(
@ -145,6 +146,24 @@ export class MarkitdownConverter {
};
}
// Apply post-conversion hooks (frontmatter, tags)
if (options?.postProcess) {
try {
const raw = fs.readFileSync(outputPath, 'utf-8');
const processed = applyPostConversionHooks(
raw,
options.postProcess.inputPath,
options.postProcess.settings
);
if (processed !== raw) {
fs.writeFileSync(outputPath, processed, 'utf-8');
}
} catch (hookError: unknown) {
// Log but don't fail the conversion for a post-processing error
console.warn('markitdown: post-conversion hook error:', hookError);
}
}
// Parse success response
let imagesExtracted = 0;
try {

View file

@ -163,6 +163,32 @@ export class SettingsTab extends PluginSettingTab {
await this.plugin.saveSettings();
}));
// ── Post-conversion ─────────────────────
new Setting(containerEl)
.setName('Post-conversion')
.setHeading();
new Setting(containerEl)
.setName('Auto frontmatter')
.setDesc('Automatically add YAML frontmatter with source filename, conversion date, and converter name')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.enableAutoFrontmatter)
.onChange(async (value) => {
this.plugin.settings.enableAutoFrontmatter = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Auto tags')
.setDesc('Comma-separated tags to add to frontmatter (e.g., "converted, imported, pdf"). Requires auto frontmatter or adds a frontmatter block for tags alone.')
.addText(text => text
.setPlaceholder('converted, imported')
.setValue(this.plugin.settings.autoTags)
.onChange(async (value) => {
this.plugin.settings.autoTags = value;
await this.plugin.saveSettings();
}));
// ── Advanced ─────────────────────────────
new Setting(containerEl)
.setName('Advanced')

View file

@ -16,6 +16,8 @@ export interface MarkitdownSettings {
enableRecursiveConversion: boolean;
outputFilenameTemplate: string;
enableDragDrop: boolean;
enableAutoFrontmatter: boolean;
autoTags: string;
}
export const DEFAULT_SETTINGS: MarkitdownSettings = {
@ -31,6 +33,8 @@ export const DEFAULT_SETTINGS: MarkitdownSettings = {
enableRecursiveConversion: false,
outputFilenameTemplate: '{filename}',
enableDragDrop: true,
enableAutoFrontmatter: false,
autoTags: '',
};
export interface ConversionOptions {
@ -39,6 +43,10 @@ export interface ConversionOptions {
docintelEndpoint?: string;
extractImages?: boolean;
imageDir?: string;
postProcess?: {
settings: MarkitdownSettings;
inputPath: string;
};
}
export interface ConversionResult {

154
src/utils/postprocess.ts Normal file
View file

@ -0,0 +1,154 @@
import * as path from 'path';
import { MarkitdownSettings } from '../types/settings';
/**
* Escape a string value for safe inclusion in YAML.
* Wraps in double quotes if the value contains characters that could
* break YAML parsing.
*/
function yamlEscape(value: string): string {
if (/[:#\[\]{}&*!|>'"`,@%\\]/.test(value) || value.trim() !== value) {
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}
return value;
}
/**
* Build frontmatter fields based on settings and input file.
*/
function buildFrontmatterFields(
inputPath: string,
settings: MarkitdownSettings
): Record<string, string | string[]> {
const fields: Record<string, string | string[]> = {};
if (settings.enableAutoFrontmatter) {
const filename = path.basename(inputPath);
const now = new Date();
const yyyy = now.getFullYear();
const mm = String(now.getMonth() + 1).padStart(2, '0');
const dd = String(now.getDate()).padStart(2, '0');
fields['source'] = filename;
fields['converted'] = `${yyyy}-${mm}-${dd}`;
fields['converter'] = 'markitdown';
}
if (settings.autoTags.trim()) {
const tags = settings.autoTags
.split(',')
.map(t => t.trim())
.filter(t => t.length > 0);
if (tags.length > 0) {
fields['tags'] = tags;
}
}
return fields;
}
/**
* Serialize a record of fields into YAML frontmatter lines (without delimiters).
*/
function serializeFields(fields: Record<string, string | string[]>): string {
const lines: string[] = [];
for (const [key, value] of Object.entries(fields)) {
if (Array.isArray(value)) {
lines.push(`${key}:`);
for (const item of value) {
lines.push(` - ${yamlEscape(item)}`);
}
} else {
lines.push(`${key}: ${yamlEscape(value)}`);
}
}
return lines.join('\n');
}
/**
* Parse an existing YAML frontmatter block.
* Returns the raw YAML body (without delimiters) and the rest of the content.
* Returns null if no frontmatter is found.
*/
function parseExistingFrontmatter(content: string): {
yaml: string;
body: string;
} | null {
// Frontmatter must start at the very beginning of the file
if (!content.startsWith('---')) return null;
// Find the closing delimiter — skip the first line
const closingIndex = content.indexOf('\n---', 3);
if (closingIndex === -1) return null;
const yaml = content.substring(4, closingIndex).trim(); // after "---\n"
// Body starts after the closing "---" line
const afterClosing = closingIndex + 4; // skip "\n---"
const body = content.substring(afterClosing);
return { yaml, body };
}
/**
* Merge new fields into existing YAML frontmatter text.
* Only adds fields that don't already exist (to avoid overwriting user edits).
*/
function mergeIntoYaml(
existingYaml: string,
fields: Record<string, string | string[]>
): string {
const existingKeys = new Set<string>();
for (const line of existingYaml.split('\n')) {
const match = line.match(/^(\w[\w-]*)\s*:/);
if (match) {
existingKeys.add(match[1]);
}
}
const newFields: Record<string, string | string[]> = {};
for (const [key, value] of Object.entries(fields)) {
if (!existingKeys.has(key)) {
newFields[key] = value;
}
}
if (Object.keys(newFields).length === 0) {
return existingYaml;
}
const additional = serializeFields(newFields);
return existingYaml + '\n' + additional;
}
/**
* Apply post-conversion hooks to the converted Markdown content.
*
* - Adds/merges YAML frontmatter with source metadata if enabled.
* - Adds tags to frontmatter if configured.
*
* Returns the (possibly modified) content string.
*/
export function applyPostConversionHooks(
content: string,
inputPath: string,
settings: MarkitdownSettings
): string {
const fields = buildFrontmatterFields(inputPath, settings);
// Nothing to add
if (Object.keys(fields).length === 0) {
return content;
}
const existing = parseExistingFrontmatter(content);
if (existing) {
// Merge into existing frontmatter
const merged = mergeIntoYaml(existing.yaml, fields);
return `---\n${merged}\n---${existing.body}`;
}
// Prepend new frontmatter block
const yaml = serializeFields(fields);
return `---\n${yaml}\n---\n\n${content}`;
}