mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-02-24 04:00:59 +01:00
feat: add standard notes support in importer (#285)
* add support for standard notes in importer * rename types * move editors to constants.ts * change ContentTypes to enum * make all properties of SNBackup optional * add version & backup file check * refactor buildTableWithRows function * rename mode to language * use inverted condition to reduce indent * refactor table construction html strings
This commit is contained in:
7588
package-lock.json
generated
7588
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Binary file not shown.
23
packages/importer/package-lock.json
generated
23
packages/importer/package-lock.json
generated
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@notesnook/enex": "^1.0.2",
|
||||
"fflate": "^0.7.1",
|
||||
"highlight.js": "^11.4.0",
|
||||
"node-html-parser": "github:thecodrr/node-html-parser",
|
||||
"showdown": "github:thecodrr/showdown",
|
||||
"spark-md5": "^3.0.2"
|
||||
@@ -1647,6 +1648,23 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/hasha/node_modules/type-fest": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
|
||||
"integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/highlight.js": {
|
||||
"version": "11.4.0",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.4.0.tgz",
|
||||
"integrity": "sha512-nawlpCBCSASs7EdvZOYOYVkJpGmAOKMYZgZtUqSRqodZE0GRVcFKwo1RcpeOemqh9hyttTdd5wDBwHkuSyUfnA==",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
@@ -6476,6 +6494,11 @@
|
||||
"type-fest": "^0.8.0"
|
||||
}
|
||||
},
|
||||
"highlight.js": {
|
||||
"version": "11.4.0",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.4.0.tgz",
|
||||
"integrity": "sha512-nawlpCBCSASs7EdvZOYOYVkJpGmAOKMYZgZtUqSRqodZE0GRVcFKwo1RcpeOemqh9hyttTdd5wDBwHkuSyUfnA=="
|
||||
},
|
||||
"html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@notesnook/enex": "^1.0.2",
|
||||
"fflate": "^0.7.1",
|
||||
"highlight.js": "^11.4.0",
|
||||
"node-html-parser": "github:thecodrr/node-html-parser",
|
||||
"showdown": "github:thecodrr/showdown",
|
||||
"spark-md5": "^3.0.2"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { StandardNotes } from './standardnotes';
|
||||
import { Evernote } from "./evernote";
|
||||
import { Markdown } from "./md";
|
||||
import { HTML } from "./html";
|
||||
@@ -11,6 +12,7 @@ const providerMap = {
|
||||
html: HTML,
|
||||
keep: GoogleKeep,
|
||||
simplenote: Simplenote,
|
||||
standardnotes:StandardNotes
|
||||
};
|
||||
export type Providers = keyof typeof providerMap;
|
||||
|
||||
|
||||
42
packages/importer/src/providers/standardnotes/constants.ts
Normal file
42
packages/importer/src/providers/standardnotes/constants.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { EditorType } from './types';
|
||||
export const editors: {
|
||||
[name: string]: EditorType;
|
||||
} = {
|
||||
"Vim Editor": {
|
||||
type: "code",
|
||||
},
|
||||
"Secure Spreadsheets": {
|
||||
type: "json",
|
||||
jsonFormat: "table",
|
||||
},
|
||||
"Minimal Markdown Editor": {
|
||||
type: "markdown",
|
||||
},
|
||||
"Fancy Markdown Editor": {
|
||||
type: "markdown",
|
||||
},
|
||||
"Advanced Markdown Editor": {
|
||||
type: "markdown",
|
||||
},
|
||||
TokenVault: {
|
||||
type: "json",
|
||||
jsonFormat: "token",
|
||||
},
|
||||
"Plus Editor": {
|
||||
type: "html",
|
||||
},
|
||||
"Simple Task Editor": {
|
||||
type: "markdown",
|
||||
},
|
||||
"Code Editor": {
|
||||
type: "code",
|
||||
},
|
||||
"Bold Editor": {
|
||||
type: "html",
|
||||
},
|
||||
"Simple Markdown Editor": {
|
||||
type: "markdown",
|
||||
},
|
||||
};
|
||||
|
||||
export const SNBackupVersion = "004";
|
||||
235
packages/importer/src/providers/standardnotes/index.ts
Normal file
235
packages/importer/src/providers/standardnotes/index.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import hljs from "highlight.js";
|
||||
import showdown from "showdown";
|
||||
import { Content, ContentType, Note } from "../../models/note";
|
||||
import { File } from "../../utils/file";
|
||||
import { TransformError } from "../../utils/transformerror";
|
||||
import {
|
||||
IProvider,
|
||||
iterate,
|
||||
ProviderResult,
|
||||
ProviderSettings,
|
||||
} from "../provider";
|
||||
import { editors, SNBackupVersion } from "./constants";
|
||||
import {
|
||||
ContentTypes,
|
||||
EditorType,
|
||||
SNBackup,
|
||||
SNBackupItem,
|
||||
SpreadSheet,
|
||||
TokenVaultItem,
|
||||
} from "./types";
|
||||
import {
|
||||
buildCell,
|
||||
buildRow,
|
||||
buildTableWithRows,
|
||||
} from "../../utils/tablebuilder";
|
||||
import { buildCodeBlock } from "../../utils//codebuilder";
|
||||
|
||||
const converter = new showdown.Converter();
|
||||
export class StandardNotes implements IProvider {
|
||||
public supportedExtensions = [".txt"];
|
||||
public validExtensions = [...this.supportedExtensions];
|
||||
public version = "1.0.0";
|
||||
public name = "StandardNotes";
|
||||
|
||||
async process(
|
||||
files: File[],
|
||||
settings: ProviderSettings
|
||||
): Promise<ProviderResult> {
|
||||
return iterate(this, files, (file, notes, errors) => {
|
||||
if (file.name !== "Standard Notes Backup and Import File.txt")
|
||||
return Promise.resolve(true);
|
||||
let data: SNBackup = <SNBackup>JSON.parse(file.text);
|
||||
if (!data.items) {
|
||||
errors.push(new TransformError("Backup file is invalid", file));
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
if (data.version !== SNBackupVersion) {
|
||||
errors.push(
|
||||
new TransformError("Backup version is not supported.", file)
|
||||
);
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
const components: SNBackupItem[] = [];
|
||||
const tags: SNBackupItem[] = [];
|
||||
const snNotes: SNBackupItem[] = [];
|
||||
|
||||
data.items?.forEach((item) => {
|
||||
let contentType = item.content_type;
|
||||
switch (contentType) {
|
||||
case ContentTypes.Note:
|
||||
snNotes.push(item);
|
||||
case ContentTypes.Component:
|
||||
components.push(item);
|
||||
break;
|
||||
case ContentTypes.SmartTag:
|
||||
case ContentTypes.Tag:
|
||||
tags.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
for (let item of snNotes) {
|
||||
let type = this.getContentType(item, components);
|
||||
item.content.editorType = type;
|
||||
let note: Note = {
|
||||
title: item.content.title,
|
||||
dateCreated: item.created_at_timestamp,
|
||||
dateEdited: item.updated_at_timestamp || item.created_at_timestamp,
|
||||
pinned: item.content.appData["org.standardnotes.sn"]?.pinned,
|
||||
tags: this.getTags(item, tags),
|
||||
content: this.parseContent(item),
|
||||
};
|
||||
notes.push(note);
|
||||
}
|
||||
|
||||
return Promise.resolve(true);
|
||||
});
|
||||
}
|
||||
|
||||
getContentType(item: SNBackupItem, components: SNBackupItem[]): EditorType {
|
||||
let componentData =
|
||||
item.content.appData["org.standardnotes.sn.components"] || {};
|
||||
let editorId = Object.keys(componentData).pop();
|
||||
if (editorId) {
|
||||
let editor = components.find((component) => component.uuid === editorId);
|
||||
if (editor) {
|
||||
let contentType = {
|
||||
...editors[editor.content.name],
|
||||
};
|
||||
if (contentType.type === "code") {
|
||||
//@ts-ignore
|
||||
contentType.mode = componentData[editorId].mode;
|
||||
}
|
||||
|
||||
return contentType;
|
||||
}
|
||||
}
|
||||
return {
|
||||
type:
|
||||
item.content.text.includes("<") && item.content.text.includes("</")
|
||||
? "html"
|
||||
: "text",
|
||||
};
|
||||
}
|
||||
|
||||
getTags(item: SNBackupItem, tags: SNBackupItem[]): string[] {
|
||||
if (!item.content.references || item.content.references.length === 0)
|
||||
return [];
|
||||
let references = item.content.references;
|
||||
|
||||
let noteTags = [];
|
||||
for (let reference of references) {
|
||||
let tag = tags.find((tag) => tag.uuid === reference.uuid);
|
||||
if (tag && tag.content.name && tag.content.name.trim() !== "") {
|
||||
noteTags.push(tag.content.name);
|
||||
}
|
||||
}
|
||||
return noteTags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find object in array with maximum index
|
||||
* @param array
|
||||
* @returns
|
||||
*/
|
||||
maxIndexItem(array: any[]) {
|
||||
return array.reduce((prev, current) =>
|
||||
prev.index > current.index ? prev : current
|
||||
);
|
||||
}
|
||||
|
||||
parseContent(item: SNBackupItem): Content {
|
||||
const data = item.content.text;
|
||||
const editorType = item.content.editorType;
|
||||
switch (editorType.type) {
|
||||
case "text":
|
||||
return {
|
||||
data: converter.makeHtml(data),
|
||||
type: ContentType.HTML,
|
||||
};
|
||||
case "html":
|
||||
return {
|
||||
data: data,
|
||||
type: ContentType.HTML,
|
||||
};
|
||||
case "markdown":
|
||||
return {
|
||||
data: converter.makeHtml(data),
|
||||
type: ContentType.HTML,
|
||||
};
|
||||
case "code":
|
||||
let language = editorType.language || "plaintext";
|
||||
if (language === "htmlmixed") language = "html";
|
||||
let code = hljs.highlightAuto(data, [language]);
|
||||
let html = buildCodeBlock(code.value, language);
|
||||
return {
|
||||
type: ContentType.HTML,
|
||||
data: html,
|
||||
};
|
||||
case "json":
|
||||
if (editorType.jsonFormat === "token") {
|
||||
let tokens = <TokenVaultItem[]>JSON.parse(data);
|
||||
|
||||
let html = `
|
||||
${tokens.map((token) => {
|
||||
let keys = Object.keys(token);
|
||||
|
||||
buildTableWithRows(
|
||||
keys.map((key) =>
|
||||
buildRow([buildCell(key, "th"), buildCell(token[key])])
|
||||
)
|
||||
);
|
||||
})}`;
|
||||
return {
|
||||
data: html,
|
||||
type: ContentType.HTML,
|
||||
};
|
||||
} else {
|
||||
let spreadsheet = <SpreadSheet>JSON.parse(data);
|
||||
let html = ``;
|
||||
for (let sheet of spreadsheet.sheets) {
|
||||
if (!sheet.rows || sheet.rows.length === 0) continue;
|
||||
let maxCols =
|
||||
this.maxIndexItem(
|
||||
sheet.rows.map((row) => {
|
||||
return this.maxIndexItem(row.cells);
|
||||
})
|
||||
).index + 1;
|
||||
|
||||
let maxRows = this.maxIndexItem(sheet.rows).index + 1;
|
||||
let rows = [];
|
||||
|
||||
for (let i = 0; i < maxRows; i++) {
|
||||
let rowAtIndex = sheet.rows.find((row) => row.index === i);
|
||||
// create an empty row to fill index
|
||||
if (!rowAtIndex) rowAtIndex = { index: i, cells: [] };
|
||||
let cells = [];
|
||||
for (let col = 0; col < maxCols; col++) {
|
||||
let cellAtCol = rowAtIndex.cells.find(
|
||||
(cell) => cell.index === col
|
||||
);
|
||||
// create an empty cell to fill index
|
||||
if (!cellAtCol) cellAtCol = { value: "", index: col };
|
||||
cells.push(buildCell(cellAtCol.value));
|
||||
}
|
||||
rows.push(buildRow(cells));
|
||||
}
|
||||
|
||||
let table = buildTableWithRows(rows);
|
||||
html = html + table;
|
||||
}
|
||||
return {
|
||||
type: ContentType.HTML,
|
||||
data: html,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return {
|
||||
data: converter.makeHtml(data),
|
||||
type: ContentType.HTML,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
75
packages/importer/src/providers/standardnotes/types.ts
Normal file
75
packages/importer/src/providers/standardnotes/types.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
|
||||
export enum ContentTypes {
|
||||
Note = "Note",
|
||||
Tag = "Tag",
|
||||
SmartTag = "SN|SmartTag",
|
||||
Component = "SN|Component"
|
||||
}
|
||||
|
||||
export type EditorType = {
|
||||
type: "code" | "html" | "text" | "json" | "markdown";
|
||||
jsonFormat?: "table" | "token";
|
||||
language?: string;
|
||||
};
|
||||
|
||||
export type SpreadSheet = {
|
||||
sheets: { rows: Row[] }[];
|
||||
};
|
||||
|
||||
export interface Row {
|
||||
index: number;
|
||||
cells: Cell[];
|
||||
}
|
||||
|
||||
export interface Cell {
|
||||
value: number | string;
|
||||
index: number;
|
||||
format?: string;
|
||||
bold?: boolean;
|
||||
}
|
||||
|
||||
export type TokenVaultItem = {
|
||||
[name: string]: string;
|
||||
service: string;
|
||||
account: string;
|
||||
secret: string;
|
||||
notes: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type SNBackup = {
|
||||
version?: string;
|
||||
items?: SNBackupItem[];
|
||||
};
|
||||
|
||||
export type SNBackupItem = {
|
||||
uuid: string;
|
||||
content_type: ContentTypes;
|
||||
content: {
|
||||
title: string;
|
||||
text: string;
|
||||
name: string;
|
||||
editorType: EditorType;
|
||||
references: {
|
||||
uuid: string;
|
||||
content_type: string;
|
||||
}[];
|
||||
appData: {
|
||||
"org.standardnotes.sn": {
|
||||
client_updated_at: string;
|
||||
pinned: boolean;
|
||||
prefersPlainEditor: boolean;
|
||||
archived?: boolean;
|
||||
defaultEditor?: boolean;
|
||||
};
|
||||
"org.standardnotes.sn.components": { [name: string]: string };
|
||||
};
|
||||
preview_html: string;
|
||||
preview_plain: string;
|
||||
};
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_at_timestamp: number;
|
||||
updated_at_timestamp: number;
|
||||
duplicate_of: null;
|
||||
};
|
||||
5
packages/importer/src/utils/codebuilder.ts
Normal file
5
packages/importer/src/utils/codebuilder.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function buildCodeBlock(value: string, language: string) {
|
||||
return `<pre class="hljs language-${language}">
|
||||
${value}
|
||||
</pre>`;
|
||||
}
|
||||
18
packages/importer/src/utils/tablebuilder.ts
Normal file
18
packages/importer/src/utils/tablebuilder.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export function buildTableWithRows(rows: string[]) {
|
||||
return `<div class="table-container" contenteditable="false">
|
||||
<table contenteditable="true">
|
||||
<tbody>
|
||||
${rows.join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p><br data-mce-bogus="1"/></p>`;
|
||||
}
|
||||
|
||||
export function buildCell(value: string | number, type = "td") {
|
||||
return `<${type}>${value || ""}</${type}>`;
|
||||
}
|
||||
|
||||
export function buildRow(cells: string[]) {
|
||||
return `<tr>${cells.join("")}</tr>`;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user