Skip to content

Commit

Permalink
Merge pull request #95 from dvcrn/93-heading-inside-block
Browse files Browse the repository at this point in the history
  • Loading branch information
dvcrn authored Dec 25, 2024
2 parents 76470ef + d169e4f commit 285ab20
Show file tree
Hide file tree
Showing 9 changed files with 2,367 additions and 99 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ jobs:
${{ runner.os }}-yarn-
- name: yarn install
run: yarn install --frozen-lockfile
- name: Run test
run: yarn test
- name: Run Build
run: yarn run build
- name: Checking format
Expand Down
55 changes: 55 additions & 0 deletions __mocks__/obsidian.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
export interface PluginManifest {
id: string;
name: string;
version: string;
minAppVersion: string;
author: string;
description: string;
}

export class App {
vault: any;
workspace: any;
constructor() {
this.vault = {};
this.workspace = {};
}
}

export class Plugin {
app: App;
manifest: any;

constructor(app: App, manifest: any) {
this.app = app;
this.manifest = manifest;
}

loadData(): Promise<any> {
return Promise.resolve({});
}

saveData(data: any): Promise<void> {
return Promise.resolve();
}
}

export class PluginSettingTab {}
export class Setting {}
export class TAbstractFile {}
export class TFile extends TAbstractFile {
basename: string;
extension: string;
path: string;
parent: any;

constructor() {
super();
this.basename = '';
this.extension = '';
this.path = '';
this.parent = { path: '' };
}
}
export class Editor {}
export class MarkdownView {}
6 changes: 2 additions & 4 deletions exclusions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ export function isExcalidraw(app: App, f: TFile) {

export function isKanban(app: App, f: TFile) {
const fileCache = app.metadataCache.getFileCache(f);
return (
!!fileCache?.frontmatter && !!fileCache.frontmatter['kanban-plugin']
);
return !!fileCache?.frontmatter && !!fileCache.frontmatter['kanban-plugin'];
}

export function isExcluded(app: App, f: TFile) {
Expand All @@ -24,6 +22,6 @@ export function isExcluded(app: App, f: TFile) {
if (isKanban(app, f)) {
return true;
}

return false;
}
16 changes: 16 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleNameMapper: {
'^obsidian': '<rootDir>/node_modules/obsidian/dist/obsidian.js',
},
transform: {
'^.+\\.tsx?': [
'ts-jest',
{
tsconfig: 'tsconfig.json',
},
],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
};
137 changes: 137 additions & 0 deletions main.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import FilenameHeadingSyncPlugin from './main';
import { App, PluginManifest } from 'obsidian';

describe('FilenameHeadingSyncPlugin', () => {
let plugin: FilenameHeadingSyncPlugin;
let app: App;

beforeEach(() => {
// Create a proper mock of the App class
app = {
vault: {
on: jest.fn(),
getFiles: jest.fn().mockReturnValue([]),
},
workspace: {
on: jest.fn(),
activeLeaf: null,
},
fileManager: {},
// Add other required App properties
} as unknown as App;

const manifest: PluginManifest = {
id: 'test',
name: 'Test Plugin',
version: '1.0.0',
minAppVersion: '0.15.0',
author: 'Test Author',
description: 'Test Description',
};

plugin = new FilenameHeadingSyncPlugin(app, manifest);
});

describe('findHeading', () => {
it('should find heading after frontmatter', () => {
const fileLines = [
'---',
'title: Test',
'date: 2023-01-01',
'---',
'# First Heading',
'Some content',
];

const result = plugin.findHeading(
fileLines,
plugin.findNoteStart(fileLines),
);

expect(result).not.toBeNull();
expect(result?.text).toBe('First Heading');
expect(result?.style).toBe('Prefix');
expect(result?.lineNumber).toBe(4);
});

it('should skip heading in fenced code block', () => {
const fileLines = [
'',
'```',
'# Heading Inside Code',
'```',
'# Actual Heading',
'Some content',
];

const result = plugin.findHeading(fileLines, 0);

expect(result).not.toBeNull();
expect(result?.text).toBe('Actual Heading');
expect(result?.style).toBe('Prefix');
expect(result?.lineNumber).toBe(4);
});

it('should find underline style heading', () => {
const fileLines = ['First Heading', '============', 'Some content'];

const result = plugin.findHeading(fileLines, 0);

expect(result).not.toBeNull();
expect(result?.text).toBe('First Heading');
expect(result?.style).toBe('Underline');
expect(result?.lineNumber).toBe(0);
});

it('should ignore escaped hash prefix', () => {
const fileLines = [
'\\# Not a heading',
'# Actual Heading',
'Some content',
];

const result = plugin.findHeading(fileLines, 0);

expect(result).not.toBeNull();
expect(result?.text).toBe('Actual Heading');
expect(result?.style).toBe('Prefix');
expect(result?.lineNumber).toBe(1);
});

it('should ignore hash inside inline code', () => {
const fileLines = [
'`# Not a heading`',
'# Actual Heading',
'Some content',
];

const result = plugin.findHeading(fileLines, 0);

expect(result).not.toBeNull();
expect(result?.text).toBe('Actual Heading');
expect(result?.style).toBe('Prefix');
expect(result?.lineNumber).toBe(1);
});

it('should ignore heading inside frontmatter', () => {
const fileLines = [
'---',
'# Not a heading',
'title: Test',
'---',
'# Actual Heading',
'Some content',
];

const result = plugin.findHeading(
fileLines,
plugin.findNoteStart(fileLines),
);

expect(result).not.toBeNull();
expect(result?.text).toBe('Actual Heading');
expect(result?.style).toBe('Prefix');
expect(result?.lineNumber).toBe(4);
});
});
});
75 changes: 52 additions & 23 deletions main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
TFile,
Editor,
MarkdownView,
PluginManifest,
} from 'obsidian';
import { isExcluded } from './exclusions';

Expand Down Expand Up @@ -85,9 +86,10 @@ export default class FilenameHeadingSyncPlugin extends Plugin {
let leaf = this.app.workspace.activeLeaf;
if (leaf) {
if (!checking) {
this.settings.ignoredFiles[
this.app.workspace.getActiveFile().path
] = null;
const activeFile = this.app.workspace.getActiveFile();
if (activeFile) {
this.settings.ignoredFiles[activeFile.path] = null;
}
this.saveSettings();
}
return true;
Expand Down Expand Up @@ -125,7 +127,7 @@ export default class FilenameHeadingSyncPlugin extends Plugin {
// check regex
try {
if (this.settings.ignoreRegex === '') {
return;
return false;
}

const reg = new RegExp(this.settings.ignoreRegex);
Expand Down Expand Up @@ -276,27 +278,57 @@ export default class FilenameHeadingSyncPlugin extends Plugin {
* @returns {LinePointer | null} LinePointer to heading or null if no heading found
*/
findHeading(fileLines: string[], startLine: number): LinePointer | null {
let insideCodeBlock = false;

for (let i = startLine; i < fileLines.length; i++) {
if (fileLines[i].startsWith('# ')) {
return {
lineNumber: i,
text: fileLines[i].substring(2),
style: HeadingStyle.Prefix,
};
} else {
if (
fileLines[i + 1] !== undefined &&
fileLines[i + 1].match(/^=+$/) !== null
) {
const line = fileLines[i].trimEnd(); // Trim end to handle spaces after backticks

// Check for fenced code block markers (allowing for indentation)
if (line.trim().startsWith('```')) {
insideCodeBlock = !insideCodeBlock;
continue;
}

// Skip if inside code block
if (insideCodeBlock) {
continue;
}

// Check for prefix style heading, ignoring escaped hashes and inline code
if (line.startsWith('# ')) {
// Skip if the line is inside inline code (has odd number of backticks before #)
const backtickCount = (line.match(/`/g) || []).length;
if (backtickCount % 2 === 0) {
// Skip if the # is escaped with \
if (!line.startsWith('\\# ')) {
return {
lineNumber: i,
text: line.substring(2),
style: HeadingStyle.Prefix,
};
}
}
}

// Check for underline style heading
if (
fileLines[i + 1] !== undefined &&
fileLines[i + 1].trim().match(/^=+$/) !== null &&
line.length > 0 // Ensure the heading line isn't empty
) {
// Skip if the line is inside inline code
const backtickCount = (line.match(/`/g) || []).length;
if (backtickCount % 2 === 0) {
return {
lineNumber: i,
text: fileLines[i],
text: line,
style: HeadingStyle.Underline,
};
}
}
}
return null; // no heading found

return null; // no valid heading found outside code blocks
}

regExpEscape(str: string): string {
Expand Down Expand Up @@ -545,12 +577,10 @@ class FilenameHeadingSyncSettingTab extends PluginSettingTab {

containerEl.createEl('h2', { text: 'Filename Heading Sync' });
containerEl.createEl('p', {
text:
'This plugin will overwrite the first heading found in a file with the filename.',
text: 'This plugin will overwrite the first heading found in a file with the filename.',
});
containerEl.createEl('p', {
text:
'If no header is found, will insert a new one at the first line (after frontmatter).',
text: 'If no header is found, will insert a new one at the first line (after frontmatter).',
});

new Setting(containerEl)
Expand Down Expand Up @@ -678,8 +708,7 @@ class FilenameHeadingSyncSettingTab extends PluginSettingTab {

containerEl.createEl('h2', { text: 'Manually Ignored Files' });
containerEl.createEl('p', {
text:
'You can ignore files from this plugin by using the "ignore this file" command',
text: 'You can ignore files from this plugin by using the "ignore this file" command',
});

// go over all ignored files and add them
Expand Down
13 changes: 7 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,21 @@
"scripts": {
"dev": "rollup --config rollup.config.js -w",
"build": "rollup --config rollup.config.js",
"format": "npx prettier --config .prettierrc --write *.ts"
"format": "npx prettier --config .prettierrc --write *.ts",
"test": "jest"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@rollup/plugin-commonjs": "^15.1.0",
"@rollup/plugin-node-resolve": "^9.0.0",
"@rollup/plugin-typescript": "^6.0.0",
"@types/node": "^14.14.2",
"@types/jest": "^29.5.14",
"@types/node": "^20.8.0",
"jest": "^29.7.0",
"obsidian": "^0.12.11",
"prettier": "^2.2.1",
"rollup": "^2.32.1",
"ts-jest": "^29.2.5",
"tslib": "^2.0.3",
"typescript": "^4.0.3"
"typescript": "^5.2.2"
}
}
Loading

0 comments on commit 285ab20

Please sign in to comment.