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:
Ammar Ahmed
2022-01-20 10:49:29 +05:00
committed by GitHub
parent b4e58139e3
commit 8cefa1defc
11 changed files with 3755 additions and 4307 deletions

7588
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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"

View File

@@ -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;

View 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";

View 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,
};
}
}
}

View 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;
};

View File

@@ -0,0 +1,5 @@
export function buildCodeBlock(value: string, language: string) {
return `<pre class="hljs language-${language}">
${value}
</pre>`;
}

View 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