Skip to content

Commit

Permalink
Merge pull request #15 from decaf-dev/dev
Browse files Browse the repository at this point in the history
1.2.0
  • Loading branch information
decaf-dev authored Aug 19, 2024
2 parents 85e111c + 3596bdb commit f755a59
Show file tree
Hide file tree
Showing 20 changed files with 502 additions and 138 deletions.
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

0 comments on commit f755a59

Please sign in to comment.