Skip to content

Commit

Permalink
add recent logs to settings, fix property deletion bug, add folder su…
Browse files Browse the repository at this point in the history
…ggestions to save location setting
  • Loading branch information
inhumantsar committed Apr 23, 2024
1 parent 09757c1 commit 9a2396a
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 129 deletions.
33 changes: 0 additions & 33 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,32 +94,6 @@ For example, if you wanted a checkbox which indicates whether or not you've read

Obsidian can display booleans as checkboxes, though it may display it as text at first. This can be fixed in Reading Mode by clicking the icon next to the property and changing its type to *Checkbox*.


# Roadmap

## Toward v1

* [x] Add settings to customize and selectively disable properties.
* [x] Improve documentation and project structure.
* [ ] *IN PROGRESS* Browser extension for one-click slurps.
* [ ] *IN PROGRESS* Add setting for default [save location](https://github.com/inhumantsar/slurp/issues/9).
* [ ] Offer tag parsing, tag prefix, and save location options at slurp-time.
* [ ] Import Pocket saves, bookmarks, and more automagically
* [ ] Support for multiple authors in the byline field.
* [ ] Use a bit of custom parsing logic for popular sites to capture better data and tidy up results:
* [ ] arXiv: Authors, topics, arXiv IDs, dates, and cleaner formatting. Stretch goal: Grab the paper PDF and any code links as well.
* [ ] Medium: Clean up the author information captured, particularly the links which get spread across multiple lines currently.

## Beyond v1

* [ ] Ensure video and other embeds are captured reliably
* [ ] Integrate with an LLM to provide summaries and tag recommendations
* [ ] Make sure Slurp plays nicely with other plugins, eg Dataview
* [ ] Save PDF and/or HTML versions of the page with the Markdown versions
* [ ] More custom parsing logic
* [ ] HackerNews: Map discussion threads to blockquote levels, capture both the HN URL and the article URL, use submitter name in the byline, ensure dates are reliably captured. Stretch goal: Scores, capture article along with the discussion.
* [ ] Reddit: Literally any actual content, plus everything mentioned for HN.

# Known Issues & Limitations

* Social media links generally don't work well, for example:
Expand All @@ -129,13 +103,6 @@ Obsidian can display booleans as checkboxes, though it may display it as text at
* Slurp does *nothing* to bypass paywalls.
* The conversion will leave a bit of janky markup behind sometimes, mainly in the form of too many line breaks.

# Changelog

* 0.1.5 - Customization options for properties.
* 0.1.4 - Improve identification of note properties by sourcing them from well-known meta elements.
* 0.1.3 - Added mobile support, custom URI for bookmarklets, and the option to show all properties even when empty.
* 0.1.2 - Initial public release

# Beta Testing

If you would like to help test new features before they are officially released:
Expand Down
2 changes: 1 addition & 1 deletion manifest-beta.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "slurp",
"name": "Slurp",
"version": "0.1.7",
"version": "0.1.8",
"minAppVersion": "0.15.0",
"description": "Slurps webpages and saves them as clean, uncluttered Markdown.",
"author": "inhumantsar",
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": "slurp",
"name": "Slurp",
"version": "0.1.7",
"version": "0.1.8",
"minAppVersion": "0.15.0",
"description": "Slurps webpages and saves them as clean, uncluttered Markdown.",
"author": "inhumantsar",
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "slurp",
"version": "0.1.4",
"version": "0.1.8",
"description": "Slurps webpages and saves them as clean, uncluttered Markdown.",
"main": "main.js",
"scripts": {
Expand Down Expand Up @@ -32,4 +32,4 @@
"@mozilla/readability": "^0.5.0",
"yaml": "^2.4.1"
}
}
}
135 changes: 135 additions & 0 deletions src/file-suggester.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { TFile, App, TFolder, AbstractInputSuggest, AbstractTextComponent, TextComponent, TAbstractFile } from "obsidian";

export class FileInputSuggest extends AbstractInputSuggest<TAbstractFile> {
app: App;
inputEl: HTMLDivElement | HTMLInputElement;
callback: (value: TAbstractFile, evt: MouseEvent | KeyboardEvent) => any = () => { };
filter: "file" | "folder" | "both" = "both";

constructor(app: App, textInputEl: HTMLDivElement | HTMLInputElement) {
super(app, textInputEl);
this.inputEl = textInputEl;
this.app = app;
}

getFolders(folder?: TFolder): TFolder[] {
const f = (folder || this.app.vault.getRoot());
const filteredChildren = f.children
.filter((val): val is TFolder => val instanceof TFolder);
const childFolders = filteredChildren
.map((folder) => this.getFolders(folder));
const flatChildFolders = childFolders.flat();
return [f, ...flatChildFolders];
}

protected getSuggestions(query: string): TAbstractFile[] {
const files = [
...this.filter != "file" ? this.getFolders() : [],
...this.filter != "folder" ? this.app.vault.getFiles() : []
];
files.sort((a, b) => similarityScore(query, a.name) - similarityScore(query, b.name));
return files;
}

renderSuggestion(file: TAbstractFile, el: HTMLElement) {
el.appendText(file.path);
}

async selectSuggestion(file: TAbstractFile, evt: MouseEvent | KeyboardEvent) {
super.setValue(file.path);
this.callback(file, evt);
this.close()
}
}

export class FileInputSuggestComponent extends TextComponent {
fileInputSuggest: FileInputSuggest;

constructor(containerEl: HTMLElement, app: App) {
super(containerEl);
containerEl.appendChild(this.inputEl);
this.fileInputSuggest = new FileInputSuggest(app, this.inputEl);
}

onSelect(cb: (value: TAbstractFile, evt: MouseEvent | KeyboardEvent) => any): this {
this.fileInputSuggest.onSelect(cb);
this.fileInputSuggest.callback = cb;
return this;
}

addFilter(filter: "file" | "folder" | "both" = "both"): this {
this.fileInputSuggest.filter = filter;
return this;
}

addLimit(limit: number = 100): this {
this.fileInputSuggest.limit = limit;
return this;
}
}


const similarityScore = (query: string, path: string): number => {
// augment the edit distance with a multiplier which favours partial substring matches
const q = query.toLowerCase();
const p = path.toLowerCase();

return damerauLevenshtein(q, p) * (p.includes(q) ? 0.2 : 1) * (p.startsWith(q) ? 0.1 : 1);
}

const damerauLevenshtein = (strA: string, strB: string): number => {
// this is an implementation of the Damerau-Levenshtein (aka optimal string alignment)
// Distance algorithm for string similarity. it takes two strings and calculates the minimum
// number of edits it would take for one to match the other. it's not the most useful
// for this kind of fuzzy matching, but it works.
//
// could i have used a library with a better algo instead of implementing this myself?
// of course! but this was more fun :)

// the strings are mapped to mapped to matrix locations 1..n, so to make things
// easier to reason about, we can create new arrays which start with null
const a = [null, ...Array.from(strA.toLowerCase())];
const b = [null, ...Array.from(strB.toLowerCase())];

// init a matrix the size of a and b to hold the edit distances
const d = new Array(a.length).fill(null).map(() => Array(b.length).fill(Infinity));

// the first row and column represent distances between the ith character
// of each word and its first letter.
// t o o t h r e e
// 0 1 2 3 4 5 6 7 8
// m 1
// o 2
// 0 3
for (let i = 0; i < a.length; i++) {
d[i][0] = i;
}
for (let i = 0; i < b.length; i++) {
d[0][i] = i;
}

for (let i = 1; i < a.length; i++) {
for (let j = 1; j < b.length; j++) {
// set the base cost to 0 when the letters are the same
let cost = a[i] === b[j] ? 0 : 1

d[i][j] = Math.min(d[i - 1][j] + 1, // deletion
d[i][j - 1] + 1, // insertion
d[i - 1][j - 1] + cost) // substitution

//
if (i > 1 && j > 1 && a[i - 1] == b[j] && a[i] == b[j - 1])
d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1) // transposition
}
}

// t o o t h r e e
// 0 1 2 3 4 5 6 7 8
// m 1 1 2 3 4 5 6 7 8
// o 2 2 1 2 3 4 5 6 7
// o 3 3 2 1 2 3 4 5 6 <-- d
//
// the result, d = 6, means it would take at least 6 edits to get from "moo" to "toothree"
// or vice versa: substitute "m" for "t" plus append x5 (t-h-r-e-e)
return d[a.length - 1][b.length - 1]
}
72 changes: 41 additions & 31 deletions src/settings.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type SlurpPlugin from "main";
import { App, PluginSettingTab, Setting, ValueComponent } from "obsidian";
import { App, PluginSettingTab, Setting, TAbstractFile } from "obsidian";
import FrontMatterSettings from "./components/NotePropSettings.svelte";
import { FrontMatterProp } from "./frontmatter";
import { Logger, logger } from "./logger";
import { StringCaseOptions, type StringCase } from "./string-case";
import { DEFAULT_SETTINGS } from "./const";
import { FileInputSuggestComponent } from "./file-suggester";

export class SlurpSettingsTab extends PluginSettingTab {
plugin: SlurpPlugin;
Expand All @@ -22,18 +23,20 @@ export class SlurpSettingsTab extends PluginSettingTab {
containerEl.empty();

new Setting(containerEl).setName('General').setHeading();
this.app.workspace
new Setting(containerEl)

const saveLoc = new Setting(containerEl)
.setName('Default save location')
.setDesc("What directory should Slurp save pages to? Leave blank to save to the vault's main directory.")
.addText((text) => text
.setValue(this.plugin.settings.defaultPath)
.setPlaceholder(DEFAULT_SETTINGS.defaultPath)
.onChange(async (val) => {
this.plugin.settings.defaultPath = val;
await this.plugin.saveSettings();
})
);
.setDesc("What directory should Slurp save pages to? Leave blank to save to the vault's main directory.");

new FileInputSuggestComponent(saveLoc.controlEl, this.app)
.setValue(this.plugin.settings.defaultPath)
.setPlaceholder(DEFAULT_SETTINGS.defaultPath)
.addFilter("folder")
.addLimit(10)
.onSelect(async (val: TAbstractFile) => {
this.plugin.settings.defaultPath = val.name;
await this.plugin.saveSettings();
});

new Setting(containerEl).setName('Properties').setHeading();

Expand All @@ -50,20 +53,18 @@ export class SlurpSettingsTab extends PluginSettingTab {

const onValidate = (props: FrontMatterProp[]) => {
this.logger.debug("onValidate called", props);
// update existing
const modKeys = props.map((prop) => {
this.plugin.fmProps.set(prop.id, prop);
return prop.id;
});

// delete keys no longer present
Object.keys(this.plugin.fmProps).map((id) => modKeys
.contains(id) ? null : id).filter((id) => id !== null).map((id) => {
if (id) {
delete this.plugin.settings.fm.properties[id];
this.plugin.fmProps.delete(id);
}
});
const newPropIds = props.map((prop) => prop.id);
const deleted = Array.from(this.plugin.fmProps.keys())
.filter((id) => !newPropIds.contains(id));

if (deleted.length > 0) {
logger().warn("removing note properties", deleted);
deleted.forEach((id) => this.plugin.fmProps.delete(id));
}

// update the rest
props.forEach((prop) => this.plugin.fmProps.set(prop.id, prop));

this.plugin.saveSettings();
}
Expand Down Expand Up @@ -118,6 +119,7 @@ export class SlurpSettingsTab extends PluginSettingTab {
);


// TODO: make a component for everything below
new Setting(containerEl)
.setName("Report an Issue")
.setHeading();
Expand All @@ -133,27 +135,35 @@ export class SlurpSettingsTab extends PluginSettingTab {
})
);

let recentLogsText: HTMLTextAreaElement;
const updateLogsText = () => recentLogsText.setText(logger().dump(false, 25).content);

new Setting(containerEl)
.setName("Recent Logs")
.setDesc(
"Copy+Paste these when opening a new GitHub issue. Not available when debug mode is enabled. " +
"Submit the most recent log file instead"
"Attach the most recent log file to the GitHub issue instead."
)
.setDisabled(!this.plugin.settings.logs.debug);
.setDisabled(this.plugin.settings.logs.debug)
.addButton((btn) => btn
.setButtonText("Refresh")
.setCta()
.onClick(updateLogsText)
.setDisabled(this.plugin.settings.logs.debug));

if (!this.plugin.settings.logs.debug) {
const recentLogs = containerEl.createDiv();
const recentLogsStyles: Record<string, string> = {};
recentLogsStyles["font-size"] = "small";
recentLogs.setCssProps(recentLogsStyles);

const logsTextArea = containerEl.createEl("textarea");
logsTextArea.setText(logger().dump(false, 25).content);
recentLogsText = containerEl.createEl("textarea");
const logsTextAreaStyles: Record<string, string> = {};
logsTextAreaStyles["width"] = "100%";
logsTextAreaStyles["height"] = "20em";
logsTextArea.setCssProps(logsTextAreaStyles);
recentLogs.appendChild(logsTextArea);
recentLogsText.setCssProps(logsTextAreaStyles);
updateLogsText();
recentLogs.appendChild(recentLogsText);
containerEl.appendChild(recentLogs);
}

Expand Down
Loading

0 comments on commit 9a2396a

Please sign in to comment.