mirror of
https://github.com/ethanolivertroy/obsidian-markitdown.git
synced 2026-07-22 05:41:54 +00:00
Merge branch 'worktree-agent-aba1226f'
# Conflicts: # main.ts # src/settings/SettingsTab.ts # src/types/settings.ts
This commit is contained in:
commit
bfdab4bec2
5 changed files with 219 additions and 4 deletions
14
main.ts
14
main.ts
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
154
src/utils/postprocess.ts
Normal 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}`;
|
||||
}
|
||||
Loading…
Reference in a new issue