Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Append to split content #14

Merged
merged 3 commits into from
Aug 19, 2024
Merged
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
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,40 @@ If disabled, a timestamp will be used as the title e.g. `note-splitter-170259191
> [!NOTE]
> Disabled by default.

### Append to split content

This text will be appended to each section of split content.

**Example:**

Suppose you have two sentences and your delimiter is set to a period (`.`).

```markdown
This is sentence 1. This is sentence 2.
```

The split content would result in:

```markdown
This is sentence 1
```

```markdown
This is sentence 2
```

If you want to retain the period at the end of each sentence, simply add a period into the input field of this setting.

The updated result would be:

```markdown
This is sentence 1.
```

```markdown
This is sentence 2.
```

### Delete original

If enabled, the original note will be deleted after a successful split.
Expand Down
1 change: 1 addition & 0 deletions __mocks__/obsidian.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const normalizePath = jest.fn((path) => path);
Binary file modified bun.lockb
Binary file not shown.
8 changes: 8 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
moduleNameMapper: {
"^src/(.*)$": "<rootDir>/src/$1",
},
};
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "note-splitter",
"name": "Note Splitter",
"version": "1.1.1",
"version": "1.2.0",
"minAppVersion": "0.15.0",
"description": "Split a note into individual notes based on a delimiter.",
"author": "DecafDev",
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
{
"name": "obsidian-note-splitter",
"version": "1.1.1",
"version": "1.2.0",
"description": "Split notes based on a delimiter",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"test": "jest --config jest.config.js",
"format": "prettier --write .",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [],
"author": "DecafDev",
"license": "MIT",
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"jest": "^29.7.0",
"obsidian": "latest",
"prettier": "^3.3.2",
"ts-jest": "^29.2.4",
"tslib": "2.4.0",
"typescript": "4.7.4"
}
Expand Down
112 changes: 15 additions & 97 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import { MarkdownView, Notice, Plugin, TFile, normalizePath } from "obsidian";
import { escapeInvalidFileNameChars, removeFrontmatterBlock, trimForFileName } from "./utils";
import { MarkdownView, Notice, Plugin } from "obsidian";
import NoteSplitterSettingsTab from "./obsidian/note-splitter-settings-tab";

interface NoteSplitterSettings {
saveFolderPath: string;
useContentAsTitle: boolean;
delimiter: string;
deleteOriginalNote: boolean;
}
import { splitByDelimiter } from "./splitter/split-by-delimiter";
import { NodeFileSystem, NoteSplitterSettings, Notifier } from "./types";

const DEFAULT_SETTINGS: NoteSplitterSettings = {
saveFolderPath: "note-splitter",
useContentAsTitle: false,
delimiter: "\\n",
appendToSplitContent: "",
deleteOriginalNote: false,
};

Expand Down Expand Up @@ -40,101 +35,24 @@ export default class NoteSplitterPlugin extends Plugin {
return;
}

if (view.getMode() !== 'source') {
new Notice("Please switch to editing mode to split the note.");
if (view.getMode() !== "source") {
new Notice("Please switch to editing mode to split this note.");
return;
}

this.splitNoteByDelimiter(file);
const fileSystem: NodeFileSystem = {
create: (filePath, content) => this.app.vault.create(filePath, content),
createFolder: (folderPath) => this.app.vault.createFolder(folderPath),
delete: (file) => this.app.vault.delete(file),
read: (file) => this.app.vault.read(file),
};
const notifier: Notifier = (message: string) => new Notice(message);
await splitByDelimiter(fileSystem, notifier, file, this.settings);
},
});
}

onunload() { }

private async splitNoteByDelimiter(file: TFile) {
//Obsidian will store `\n`` as `\\n` in the settings
const delimiter = this.settings.delimiter.replace(/\\n/g, "\n");

if (delimiter === "") {
new Notice("No delimiter set. Please set a delimiter in the settings.");
return;
}

const data = await this.app.vault.cachedRead(file);

const dataWithoutFrontmatter = removeFrontmatterBlock(data);
if (dataWithoutFrontmatter === "") {
new Notice("No content to split.");
return;
}

const splitContent = dataWithoutFrontmatter
.split(delimiter)
.map((content) => content.trim())
.filter((content) => content !== "");

if (splitContent.length === 0) {
new Notice("No content to split.");
return;
}

if (splitContent.length === 1) {
new Notice("Only one piece of content found. Nothing to split.");
return;
}

const folderPath =
this.settings.saveFolderPath ||
file.parent?.path ||
this.settings.saveFolderPath;

try {
await this.app.vault.createFolder(folderPath);
} catch (err) {
//Folder already exists
}

let filesCreated = 0;
for (const [i, content] of splitContent.entries()) {
let fileName = content.split("\n")[0];
if (this.settings.useContentAsTitle) {
fileName = escapeInvalidFileNameChars(fileName);
fileName = trimForFileName(fileName, ".md");
} else {
fileName = `split-note-${Date.now() + i}`;
}

const filePath = normalizePath(`${folderPath}/${fileName}.md`);

try {
await this.app.vault.create(filePath, content);
filesCreated++;
} catch (err) {
if (err.message.includes("already exists")) {
const newFilePath = `${folderPath}/Split conflict ${crypto.randomUUID()}.md`;
try {
await this.app.vault.create(newFilePath, content);
filesCreated++;
} catch (err) {
console.error(err);
new Notice(`Error creating file: ${err.message}`);
}
continue;
}
new Notice(`Error creating file: ${err.message}`);
console.log(err);
}
}

if (filesCreated === splitContent.length && this.settings.deleteOriginalNote) {
await this.app.vault.delete(file);
}

new Notice(
"Split into " + filesCreated + " note" + (filesCreated > 1 ? "s" : "") + ".",
);
}
onunload() {}

async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
Expand Down
10 changes: 10 additions & 0 deletions src/obsidian/note-splitter-settings-tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ export default class NoteSplitterSettingsTab extends PluginSettingTab {
}),
);

new Setting(containerEl)
.setName("Append to split content")
.setDesc("Text to append to the split content.")
.addText((text) =>
text.setValue(this.plugin.settings.appendToSplitContent).onChange(async (value) => {
this.plugin.settings.appendToSplitContent = value;
await this.plugin.saveSettings();
}),
);

new Setting(containerEl)
.setName("Delete original")
.setDesc("Delete the original note after a successful split.")
Expand Down
4 changes: 4 additions & 0 deletions src/splitter/remove-frontmatter-block.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const removeFrontmatterBlock = (content: string) => {
const FRONTMATTER_REGEX = /^---[\s\S]*?---/;
return content.replace(FRONTMATTER_REGEX, "").trim();
};
22 changes: 22 additions & 0 deletions src/splitter/sanitize-file-name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Sanitizes a file name for use in a file system
*/
export const sanitizeFileName = (name: string) => {
// Replace colon with hyphen
name = name.replace(/:/g, "-");
// Replace back slash with space
name = name.replace(/\\/g, " ");
// Replace forward slash with space
name = name.replace(/\//g, " ");
// Replace carrot with nothing
name = name.replace(/\^/g, "");
// Replace left bracket with nothing
name = name.replace(/\[/g, "");
// Replace right bracket with nothing
name = name.replace(/\]/g, "");
// Replace hash tag with nothing
name = name.replace(/#/g, "");
// Replace pipe with nothing
name = name.replace(/\|/g, "");
return name.trim();
};
102 changes: 102 additions & 0 deletions src/splitter/split-by-delimiter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { normalizePath, TFile } from "obsidian";
import { removeFrontmatterBlock } from "./remove-frontmatter-block";
import { sanitizeFileName } from "./sanitize-file-name";
import { truncateFileName } from "./truncate-file-name";
import { NodeFileSystem, NoteSplitterSettings, Notifier } from "src/types";

export const splitByDelimiter = async (
fileSystem: NodeFileSystem,
notify: Notifier,
file: TFile,
{
delimiter,
saveFolderPath,
useContentAsTitle,
appendToSplitContent,
deleteOriginalNote,
}: Pick<
NoteSplitterSettings,
| "saveFolderPath"
| "delimiter"
| "useContentAsTitle"
| "appendToSplitContent"
| "deleteOriginalNote"
>,
) => {
const escapedDelimiter = delimiter.replace(/\\n/g, "\n");

if (escapedDelimiter === "") {
notify("No delimiter set. Please set a delimiter in the settings.");
return;
}

const data = await fileSystem.read(file);
const dataWithoutFrontmatter = removeFrontmatterBlock(data);
if (dataWithoutFrontmatter === "") {
notify("No content to split.");
return;
}

const splitContent = dataWithoutFrontmatter
.split(escapedDelimiter)
.map((content) => content.trim())
.filter((content) => content !== "");

if (splitContent.length === 1) {
notify("Only one section of content found. Nothing to split.");
return;
}

const folderPath = saveFolderPath || "";

try {
await fileSystem.createFolder(folderPath);
} catch (err) {
// Folder already exists
}

let filesCreated = 0;
for (const [i, originalContent] of splitContent.entries()) {
let updatedContent = originalContent;
if (appendToSplitContent.length > 0) {
updatedContent += appendToSplitContent;
}

let fileName = "";
if (useContentAsTitle) {
fileName = originalContent.split("\n")[0] + ".md";
} else {
fileName = `split-note-${Date.now() + i}.md`;
}

fileName = sanitizeFileName(fileName);
fileName = truncateFileName(fileName);

const filePath = normalizePath(`${folderPath}/${fileName}`);

try {
await fileSystem.create(filePath, updatedContent);
filesCreated++;
} catch (err) {
if (err.message.includes("already exists")) {
const newFilePath = `${folderPath}/Split conflict ${crypto.randomUUID()}.md`;
try {
await fileSystem.create(newFilePath, updatedContent);
filesCreated++;
} catch (err) {
console.error(err);
notify(`Error creating file: ${err.message}`);
}
continue;
}
notify(`Error creating file: ${err.message}`);
console.log(err);
}
}

if (deleteOriginalNote && filesCreated === splitContent.length) {
await fileSystem.delete(file);
}

notify("Split into " + filesCreated + " note" + (filesCreated > 1 ? "s" : "") + ".");
};
15 changes: 15 additions & 0 deletions src/splitter/truncate-file-name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Truncates the string to the maximum length allowed for a file name
*/
export const truncateFileName = (name: string) => {
const MAX_LENGTH = 255;

const splitArr = name.split(".");
if (splitArr.length < 2) {
throw new Error("Invalid file name");
}

const baseName = splitArr[0];
const extension = splitArr[1];
return baseName.substring(0, MAX_LENGTH - extension.length - 1) + "." + splitArr[1];
};
Loading