Skip to content

Commit

Permalink
Support JSON format as Custom dictionary files (readonly for now) (#108)
Browse files Browse the repository at this point in the history
  • Loading branch information
tadashi-aikawa committed May 4, 2022
1 parent 67bfcbe commit c44c498
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 81 deletions.
5 changes: 5 additions & 0 deletions src/model/Word.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ export interface CurrentVaultWord extends DefaultWord {
}
export interface CustomDictionaryWord extends DefaultWord {
type: "customDictionary";
caretSymbol?: string;
/** Use for inserting instead of value **/
insertedText?: string;
/** If true, ignore `Insert space after completion` option **/
ignoreSpaceAfterCompletion?: boolean;
}
export interface InternalLinkWord extends DefaultWord {
type: "internalLink";
Expand Down
134 changes: 111 additions & 23 deletions src/provider/CustomDictionaryWordProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,24 @@ import { App, FileSystemAdapter, Notice, request } from "obsidian";
import { pushWord, type WordsByFirstLetter } from "./suggester";
import type { ColumnDelimiter } from "../option/ColumnDelimiter";
import { isURL } from "../util/path";
import type { Word } from "../model/Word";
import type { CustomDictionaryWord } from "../model/Word";
import { excludeEmoji } from "../util/strings";
import type { AppHelper } from "../app-helper";

type JsonDictionary = {
/** If set, take precedence over ["Caret location symbol after complement"](https://tadashi-aikawa.github.io/docs-obsidian-various-complements-plugin/4.%20Options/4.6.%20Custom%20dictionary%20complement/%E2%9A%99%EF%B8%8FCaret%20location%20symbol%20after%20complement/) */
caretSymbol?: string;
/** If set, ignore ["Insert space after completion"](https://tadashi-aikawa.github.io/docs-obsidian-various-complements-plugin/4.%20Options/4.1.%20Main/%E2%9A%99%EF%B8%8FInsert%20space%20after%20completion/) */
ignoreSpaceAfterCompletion?: boolean;
words: {
value: string;
description?: string;
aliases?: string[];
/** If set, use this value for searching and rendering instead of `value` */
displayed?: string;
}[];
};

function escape(value: string): string {
// This tricky logics for Safari
// https://github.com/tadashi-aikawa/obsidian-various-complements-plugin/issues/56
Expand All @@ -26,23 +40,67 @@ function unescape(value: string): string {
.replace(/__VariousComplementsEscape__/g, "\\");
}

function jsonToWords(
json: JsonDictionary,
path: string,
systemCaretSymbol?: string
): CustomDictionaryWord[] {
return json.words.map((x) => ({
value: x.displayed || x.value,
description: x.description,
aliases: x.aliases,
type: "customDictionary",
createdPath: path,
insertedText: x.displayed ? x.value : undefined,
caretSymbol: json.caretSymbol ?? systemCaretSymbol,
ignoreSpaceAfterCompletion: json.ignoreSpaceAfterCompletion,
}));
}

function lineToWord(
line: string,
delimiter: ColumnDelimiter,
path: string
): Word {
const [value, description, ...aliases] = line.split(delimiter.value);
path: string,
delimiterForDisplay?: string,
delimiterForHide?: string,
systemCaretSymbol?: string
): CustomDictionaryWord {
const [v, description, ...aliases] = line.split(delimiter.value);

let value = unescape(v);
let insertedText: string | undefined;
let displayedText = value;

if (delimiterForDisplay && value.includes(delimiterForDisplay)) {
[displayedText, insertedText] = value.split(delimiterForDisplay);
}
if (delimiterForHide && value.includes(delimiterForHide)) {
insertedText = value.replace(delimiterForHide, "");
displayedText = `${value.split(delimiterForHide)[0]} ...`;
}

return {
value: unescape(value),
value: displayedText,
description,
aliases,
type: "customDictionary",
createdPath: path,
insertedText,
caretSymbol: systemCaretSymbol,
};
}

function wordToLine(word: Word, delimiter: ColumnDelimiter): string {
const escapedValue = escape(word.value);
function wordToLine(
word: CustomDictionaryWord,
delimiter: ColumnDelimiter,
dividerForDisplay: string | null
): string {
const value =
word.insertedText && dividerForDisplay
? `${word.value}${dividerForDisplay}${word.insertedText}`
: word.value;

const escapedValue = escape(value);
if (!word.description && !word.aliases) {
return escapedValue;
}
Expand All @@ -59,44 +117,69 @@ function synonymAliases(name: string): string[] {
return name === lessEmojiValue ? [] : [lessEmojiValue];
}

type Option = {
regexp: string;
delimiterForHide?: string;
delimiterForDisplay?: string;
caretSymbol?: string;
};

export class CustomDictionaryWordProvider {
private words: Word[] = [];
wordByValue: { [value: string]: Word } = {};
private words: CustomDictionaryWord[] = [];
wordByValue: { [value: string]: CustomDictionaryWord } = {};
wordsByFirstLetter: WordsByFirstLetter = {};

private appHelper: AppHelper;
private fileSystemAdapter: FileSystemAdapter;
private paths: string[];
private delimiter: ColumnDelimiter;
private dividerForDisplay: string | null;

constructor(app: App, appHelper: AppHelper) {
this.appHelper = appHelper;
this.fileSystemAdapter = app.vault.adapter as FileSystemAdapter;
}

get editablePaths(): string[] {
return this.paths.filter((x) => !isURL(x));
return this.paths.filter((x) => !isURL(x) && !x.endsWith(".json"));
}

private async loadWords(path: string, regexp: string): Promise<Word[]> {
private async loadWords(
path: string,
option: Option
): Promise<CustomDictionaryWord[]> {
const contents = isURL(path)
? await request({ url: path })
: await this.fileSystemAdapter.read(path);

return contents
.split(/\r\n|\n/)
.map((x) => x.replace(/%%.*%%/g, ""))
.filter((x) => x)
.map((x) => lineToWord(x, this.delimiter, path))
.filter((x) => !regexp || x.value.match(new RegExp(regexp)));
const words = path.endsWith(".json")
? jsonToWords(JSON.parse(contents), path, option.caretSymbol)
: contents
.split(/\r\n|\n/)
.map((x) => x.replace(/%%.*%%/g, ""))
.filter((x) => x)
.map((x) =>
lineToWord(
x,
this.delimiter,
path,
option.delimiterForDisplay,
option.delimiterForHide,
option.caretSymbol
)
);

return words.filter(
(x) => !option.regexp || x.value.match(new RegExp(option.regexp))
);
}

async refreshCustomWords(regexp: string): Promise<void> {
async refreshCustomWords(option: Option): Promise<void> {
this.clearWords();

for (const path of this.paths) {
try {
const words = await this.loadWords(path, regexp);
const words = await this.loadWords(path, option);
words.forEach((x) => this.words.push(x));
} catch (e) {
// noinspection ObjectAllocationIgnored
Expand All @@ -111,17 +194,17 @@ export class CustomDictionaryWordProvider {
}

async addWordWithDictionary(
word: Word,
word: CustomDictionaryWord,
dictionaryPath: string
): Promise<void> {
this.addWord(word);
await this.fileSystemAdapter.append(
dictionaryPath,
"\n" + wordToLine(word, this.delimiter)
"\n" + wordToLine(word, this.delimiter, this.dividerForDisplay)
);
}

private addWord(word: Word) {
private addWord(word: CustomDictionaryWord) {
// Add aliases as a synonym
const wordWithSynonym = {
...word,
Expand Down Expand Up @@ -149,8 +232,13 @@ export class CustomDictionaryWordProvider {
return this.words.length;
}

setSettings(paths: string[], delimiter: ColumnDelimiter) {
setSettings(
paths: string[],
delimiter: ColumnDelimiter,
dividerForDisplay: string | null
) {
this.paths = paths;
this.delimiter = delimiter;
this.dividerForDisplay = dividerForDisplay;
}
}
83 changes: 33 additions & 50 deletions src/ui/AutoCompleteSuggest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,8 @@ export class AutoCompleteSuggest
);
this.customDictionaryWordProvider.setSettings(
settings.customDictionaryPaths.split("\n").filter((x) => x),
ColumnDelimiter.fromName(settings.columnDelimiter)
ColumnDelimiter.fromName(settings.columnDelimiter),
settings.delimiterToDivideSuggestionsForDisplayFromInsertion || null
);

this.debounceGetSuggestions = debounce(
Expand Down Expand Up @@ -562,9 +563,15 @@ export class AutoCompleteSuggest
return;
}

await this.customDictionaryWordProvider.refreshCustomWords(
this.settings.customDictionaryWordRegexPattern
);
await this.customDictionaryWordProvider.refreshCustomWords({
regexp: this.settings.customDictionaryWordRegexPattern,
delimiterForHide: this.settings.delimiterToHideSuggestion || undefined,
delimiterForDisplay:
this.settings.delimiterToDivideSuggestionsForDisplayFromInsertion ||
undefined,
caretSymbol:
this.settings.caretLocationSymbolAfterComplement || undefined,
});

this.statusBar.setCustomDictionaryIndexed(
this.customDictionaryWordProvider.wordCount
Expand Down Expand Up @@ -836,37 +843,20 @@ export class AutoCompleteSuggest
});
}

createRenderSuggestion(word: Word): string {
const text = word.value;

if (
this.settings.delimiterToDivideSuggestionsForDisplayFromInsertion &&
text.includes(
this.settings.delimiterToDivideSuggestionsForDisplayFromInsertion
)
) {
return (
text.split(
this.settings.delimiterToDivideSuggestionsForDisplayFromInsertion
)[0] + this.settings.displayedTextSuffix
);
}
renderSuggestion(word: Word, el: HTMLElement): void {
const base = createDiv();

let text = word.value;
if (
this.settings.delimiterToHideSuggestion &&
text.includes(this.settings.delimiterToHideSuggestion)
word.type === "customDictionary" &&
word.insertedText &&
this.settings.displayedTextSuffix
) {
return `${text.split(this.settings.delimiterToHideSuggestion)[0]} ...`;
text += this.settings.displayedTextSuffix;
}

return text;
}

renderSuggestion(word: Word, el: HTMLElement): void {
const base = createDiv();

base.createDiv({
text: this.createRenderSuggestion(word),
text,
cls:
word.type === "internalLink" && word.aliasMeta
? "various-complements__suggestion-item__content__alias"
Expand Down Expand Up @@ -925,33 +915,26 @@ export class AutoCompleteSuggest
) {
insertedText = `${insertedText}, `;
} else {
if (this.settings.insertAfterCompletion) {
if (
this.settings.insertAfterCompletion &&
!(word.type === "customDictionary" && word.ignoreSpaceAfterCompletion)
) {
insertedText = `${insertedText} `;
}
}

if (
this.settings.delimiterToDivideSuggestionsForDisplayFromInsertion &&
insertedText.includes(
this.settings.delimiterToDivideSuggestionsForDisplayFromInsertion
)
) {
insertedText = insertedText.split(
this.settings.delimiterToDivideSuggestionsForDisplayFromInsertion
)[1];
}
let positionToMove = -1;

if (this.settings.delimiterToHideSuggestion) {
insertedText = insertedText.replace(
this.settings.delimiterToHideSuggestion,
""
);
}
if (word.type === "customDictionary") {
if (word.insertedText) {
insertedText = word.insertedText;
}

const caret = this.settings.caretLocationSymbolAfterComplement;
const positionToMove = caret ? insertedText.indexOf(caret) : -1;
if (positionToMove !== -1) {
insertedText = insertedText.replace(caret, "");
const caret = word.caretSymbol;
if (caret) {
positionToMove = insertedText.indexOf(caret);
insertedText = insertedText.replace(caret, "");
}
}

const editor = this.context.editor;
Expand Down
4 changes: 2 additions & 2 deletions src/ui/CustomDictionaryWordAddModal.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { App, Modal, Notice } from "obsidian";
import { AppHelper } from "../app-helper";
import type { Word } from "../model/Word";
import type { CustomDictionaryWord } from "../model/Word";
import CustomDictionaryWordAdd from "./component/CustomDictionaryWordAdd.svelte";

export class CustomDictionaryWordAddModal extends Modal {
Expand All @@ -11,7 +11,7 @@ export class CustomDictionaryWordAddModal extends Modal {
dictionaryPaths: string[],
initialValue: string = "",
dividerForDisplay: string = "",
onSubmit: (dictionaryPath: string, word: Word) => void
onSubmit: (dictionaryPath: string, word: CustomDictionaryWord) => void
) {
super(app);
const appHelper = new AppHelper(app);
Expand Down
Loading

0 comments on commit c44c498

Please sign in to comment.