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

Allow formatting of file name #105

Merged
merged 6 commits into from
Nov 15, 2021
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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ You can extract your `My Clippings.txt` file by plugging it into your computer u

- **Enriched metadata** — Enrich your notes by downloading extra metadata information about your book from Amazon.com

- **Powerful, flexible templating** — Customise your highlights to your liking by configuring your own template using ([Nunjucks][2]) templating language
- **Powerful, flexible templating with preview** — Customise your highlights and file names to your liking by configuring your own template using ([Nunjucks][2]) templating language with live preview

## Mission statement

Expand Down Expand Up @@ -76,9 +76,9 @@ your online Amazon account.
### Template tags

| Tag | Description | Present |
| -------------- | --------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| -------------- | --------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `{{text}}` | Annotated text | Always in all sync modes |
| `{{location}}` | Highlighted text location | Usually available in all sync modes. If not, then `{{page}}` will be available | Usually available. If not, then `{{page}}` will be available |
| `{{location}}` | Highlighted text location | Usually available in all sync modes. If not, then `{{page}}` will be available |
| `{{page}}` | Highlighted text page | Usually available in all sync modes. If not, then `{{location}}` will be available |
| `{{note}}` | Associated note to highlight | Optional |
| `{{appLink}}` | Link to open highlight in Kindle app | Available only for Amazon books synced through Amazon online |
Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "obsidian-kindle-plugin",
"name": "Kindle Highlights",
"version": "1.2.0",
"version": "1.3.0",
"minAppVersion": "0.10.2",
"description": "Sync your Kindle book highlights using your Amazon login or uploading your My Clippings file",
"author": "Hady Osman",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "obsidian-kindle-plugin",
"version": "1.2.0",
"version": "1.3.0",
"description": "Sync your Kindle book highlights using your Amazon login or uploading your My Clippings file",
"main": "src/index.ts",
"repository": {
Expand Down
7 changes: 3 additions & 4 deletions src/components/syncModal/views/SyncingView.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import { Jumper } from 'svelte-loading-spinners';

import { sanitizeTitleExcess } from '~/utils';
import { shortenTitle } from '~/utils';
import { currentAmazonRegion } from '~/amazonRegion';
import { store } from '../store';

Expand All @@ -13,8 +13,7 @@
} else if ($store?.syncMode === 'amazon') {
progressMessage = 'Looking for new Kindle highlights to sync...';
} else {
progressMessage =
'Parsing your My Clippings files for highlights and notes...';
progressMessage = 'Parsing your My Clippings files for highlights and notes...';
}

$: total = $store.jobs?.length;
Expand Down Expand Up @@ -42,7 +41,7 @@
<div class="kp-syncmodal--download">
Syncing
<span class="kp-syncmodal--book-name">
{sanitizeTitleExcess($store.currentJob.book.title)}
{shortenTitle($store.currentJob.book.title)}
</span>
</div>
{:else}
Expand Down
4 changes: 2 additions & 2 deletions src/fileManager/mappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import moment from 'moment';
import { get } from 'svelte/store';

import { settingsStore } from '~/store';
import { sanitizeTitle } from '~/utils';
import { fileNameRenderer } from '~/fileNameRenderer';
import type { Book, KindleFrontmatter } from '~/models';

/**
* Returns a file path for a given book relative to the current Obsidian
* vault directory.
*/
export const bookFilePath = (book: Book): string => {
const fileName = sanitizeTitle(book.title);
const fileName = fileNameRenderer.render(book);
return path.join(get(settingsStore).highlightsFolder, `${fileName}.md`);
};

Expand Down
44 changes: 44 additions & 0 deletions src/fileNameRenderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import nunjucks, { Environment } from 'nunjucks';
import sanitize from 'sanitize-filename';
import { get } from 'svelte/store';

import { shortenTitle } from '~/utils';
import { settingsStore } from '~/store';
import type { Book } from '~/models';

const DefaultTemplate = '{{shortTitle}}';

class FileNameRenderer {
private nunjucks: Environment;

constructor() {
this.nunjucks = new nunjucks.Environment(null, { autoescape: false });
}

public validate(template: string): boolean {
try {
this.nunjucks.renderString(template, {});
return true;
} catch (error) {
return false;
}
}

public render(book: Partial<Book>): string {
const template = get(settingsStore).fileNameTemplate || this.defaultTemplate();

const fileName = this.nunjucks.renderString(template, {
shortTitle: shortenTitle(book.title),
longTitle: book.title,
author: book.author,
});

return `${sanitize(fileName)}.md`;
}

public defaultTemplate(): string {
return DefaultTemplate;
}
}

export const fileNameRenderer = new FileNameRenderer();
4 changes: 2 additions & 2 deletions src/notifications.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Notice } from 'obsidian';
import { sanitizeTitle } from '~/utils';
import { shortenTitle } from '~/utils';

import { ee } from '~/eventEmitter';
import type { KindleFile } from '~/models';

export const registerNotifications = (): void => {
ee.on('resyncBook', (kindleFile) => {
new Notice(`Resyncing "${sanitizeTitle(kindleFile.book.title)}" highlights`);
new Notice(`Resyncing "${shortenTitle(kindleFile.book.title)}" highlights`);
});

ee.on('resyncComplete', (_kindleFile, diffCount) => {
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import bookTemplate from './templates/bookTemplate.njk';
import defaultHighlightTemplate from './templates/defaultHighlightTemplate.njk';
import highlightTemplateWrapper from './templates/highlightTemplateWrapper.njk';
import { BlockReferenceExtension, TrimAllEmptyLinesExtension } from './nunjucks.extensions';
import { sanitizeTitle } from '~/utils';
import { shortenTitle } from '~/utils';
import { settingsStore } from '~/store';
import { trimMultipleLines } from './helper';
import type { Book, BookHighlight, Highlight, RenderTemplate } from '~/models';
Expand Down Expand Up @@ -50,7 +50,7 @@ export class Renderer {
const params: RenderTemplate = {
...book,
fullTitle: book.title,
title: sanitizeTitle(book.title),
title: shortenTitle(book.title),
appLink: appLink(book),
...entry.metadata,
highlights: this.renderHighlights(book, highlights),
Expand Down
15 changes: 15 additions & 0 deletions src/settingsTab/fileNameTemplateSetting/components/Chip.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script lang="ts">
export let title: string;
</script>

<div class="chip setting-command-hotkeys">
<span class="setting-hotkey">{`{{${title}}}`}</span>
</div>

<style>
.chip {
display: inline-flex;
color: var(--text-normal);
margin: 0;
}
</style>
20 changes: 20 additions & 0 deletions src/settingsTab/fileNameTemplateSetting/components/Legend.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script lang="ts">
import Chip from './Chip.svelte';
</script>

<div class="wrapper">
<span class="legend">Available variables:</span>
<Chip title={'shortTitle'} />
<Chip title={'longTitle'} />
<Chip title={'author'} />
</div>

<style>
.wrapper {
margin-bottom: 10px;
}

.legend {
font-size: 0.8em;
}
</style>
48 changes: 48 additions & 0 deletions src/settingsTab/fileNameTemplateSetting/components/Preview.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<script lang="ts">
import { books, fileName, BookDemo } from '../store';

let selectedBook = $books[0];

export let onSelect: (book: BookDemo) => void;
$: onSelect(selectedBook);
</script>

<div class="wrapper">
<div class="title">Live preview</div>
<select class="dropdown" bind:value={selectedBook}>
{#each $books as book}
<option value={book}>{book.title} ({book.author})</option>
{/each}
</select>
<div class="preview">Example file name: <span class="file-name">{$fileName}</span></div>
</div>

<style>
.wrapper {
border: 1px solid var(--background-modifier-border);
display: flex;
flex-direction: column;
align-items: flex-start;
margin-top: 20px;
padding: 5px 10px;
}

.title,
.preview {
color: var(--text-muted);
}

.preview {
margin-top: 4px;
}

.file-name {
color: var(--text-normal);
}

.title,
.preview,
.dropdown {
font-size: 0.8em;
}
</style>
74 changes: 74 additions & 0 deletions src/settingsTab/fileNameTemplateSetting/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Setting } from 'obsidian';
import { get } from 'svelte/store';

import Legend from './components/Legend.svelte';
import Preview from './components/Preview.svelte';
import { settingsStore } from '~/store';
import { fileName, BookDemo } from './store';
import { fileNameRenderer } from '~/fileNameRenderer';

const updateFileNamePreview = (book: BookDemo): void => {
const renderedFileName = fileNameRenderer.render(book);
fileName.set(renderedFileName);
};

export const fileNameTemplateSetting = (el: HTMLElement): void => {
let currentBook: BookDemo;

const setting = new Setting(el)
.setName('File name template')
.setDesc(
createDocumentFragment(
'<a href="https://mozilla.github.io/nunjucks/templating.html">Nunjucks template</a> to use to format book file name'
)
)
.addText((text) => {
text.inputEl.style.width = '400px';
text.inputEl.placeholder = fileNameRenderer.defaultTemplate();

text.setValue(get(settingsStore).fileNameTemplate).onChange(async (value) => {
const isValid = fileNameRenderer.validate(value);

if (isValid) {
settingsStore.actions.setFileNameTemplate(value);
updateFileNamePreview(currentBook);
}

text.inputEl.style.border = isValid ? '' : '1px solid red';
});

return text;
});

// Tweak vertical alignment of Settings component
setting.settingEl.style.alignItems = 'normal';
setting.controlEl.style.flexDirection = 'column';
setting.controlEl.style.alignItems = 'flex-end';
setting.controlEl.style.justifyContent = 'flex-start';

// Render Legend above control text field
const legendEl = document.createElement('div');
setting.controlEl.prepend(legendEl);
new Legend({ target: legendEl });

// Render Preview box after description
new Preview({
target: setting.descEl,
props: {
onSelect: (book: BookDemo) => {
currentBook = book;
updateFileNamePreview(book);
},
},
});
};

const createDocumentFragment = (innerHtml: string): DocumentFragment => {
const descEl = document.createElement('span');
descEl.innerHTML = innerHtml;

const descFragment = document.createDocumentFragment();
descFragment.appendChild(descEl);

return descFragment;
};
13 changes: 13 additions & 0 deletions src/settingsTab/fileNameTemplateSetting/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { readable, writable } from 'svelte/store';

import type { Book } from '~/models';

export type BookDemo = Pick<Book, 'title' | 'author'>;

export const books = readable([
{ title: 'Animal Farm (Classics To Go)', author: 'George Orwell' },
{ title: 'An Everyone Culture', author: 'Robert Kegan and Lisa Laskow Lahey' },
{ title: 'The Girl on the Train: A Novel', author: 'Paula Hawkins' },
] as BookDemo[]);

export const fileName = writable('');
13 changes: 11 additions & 2 deletions src/settingsTab/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Renderer } from '~/renderer';
import { settingsStore } from '~/store';
import { scrapeLogoutUrl } from '~/scraper';
import { AmazonRegions } from '~/amazonRegion';
import { fileNameTemplateSetting } from './fileNameTemplateSetting';

const { moment } = window;

Expand All @@ -36,6 +37,7 @@ export class SettingsTab extends PluginSettingTab {
this.amazonRegion();
this.downloadBookMetadata();
this.syncOnBoot();
this.fileNameTemplate();
this.highlightTemplate();
this.sponsorMe();
}
Expand Down Expand Up @@ -128,14 +130,19 @@ export class SettingsTab extends PluginSettingTab {
});
}

private fileNameTemplate(): void {
fileNameTemplateSetting(this.containerEl);
}

private highlightTemplate(): void {
new Setting(this.containerEl)
const setting = new Setting(this.containerEl)
.setName('Highlight template')
.setDesc('Template for an individual highlight')
.addTextArea((text) => {
text.inputEl.style.width = '100%';
text.inputEl.style.height = '450px';
text.inputEl.style.height = '200px';
text.inputEl.style.fontSize = '0.8em';
text.inputEl.style.fontFamily = 'var(--font-monospace)';
text.inputEl.placeholder = this.renderer.defaultHighlightTemplate();
text.setValue(get(settingsStore).highlightTemplate).onChange(async (value) => {
const isValid = this.renderer.validate(value);
Expand All @@ -148,6 +155,8 @@ export class SettingsTab extends PluginSettingTab {
});
return text;
});

setting.settingEl.style.alignItems = 'normal';
}

private downloadBookMetadata(): void {
Expand Down
Loading