Skip to content

Commit

Permalink
fix: refactor new note modal, add validation (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
inhumantsar committed May 11, 2024
1 parent 1a76bb4 commit c6671c5
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 58 deletions.
2 changes: 2 additions & 0 deletions main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export default class SlurpPlugin extends Plugin {
displayError = (err: Error) => new Notice(`Slurp Error! ${err.message}. If this is a bug, please report it from plugin settings.`, 0);

async slurp(url: string): Promise<void> {
this.logger.debug("slurping", {url});
try {
const doc = new DOMParser().parseFromString(await fetchHtml(url), 'text/html');

Expand All @@ -138,6 +139,7 @@ export default class SlurpPlugin extends Plugin {
link: url
});
} catch (err) {
this.logger.error("Unable to Slurp page", {url, err: (err as Error).message});
this.displayError(err as Error);
}
}
Expand Down
27 changes: 27 additions & 0 deletions src/components/bouncing-progress-bar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ProgressBarComponent } from "obsidian";

export class BouncingProgressBarComponent extends ProgressBarComponent {
private timerId: number;

constructor(contentEl: HTMLElement) {
super(contentEl);
this.timerId = -1;
this.setDisabled(true);
this.setValue(0);
}

private update() {
const cur = this.getValue();
this.setValue(cur + (cur == 100 ? 1 : -1 ));
};

start() {
this.setDisabled(false);
this.timerId = window.setInterval(this.update, 10);
};

stop() {
if (this.timerId > 0) window.clearInterval(this.timerId);
}
}

11 changes: 0 additions & 11 deletions src/components/frontmatter-prop-settings.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -389,22 +389,11 @@
opacity: 50%;
}
.mod-prop .validation-error,
.mod-prop .validation-error {
color: var(--text-error);
}
.mod-prop div.validation-error {
font-size: small;
text-align: center;
margin: 0.75em 0;
}
.mod-prop div.validation-error.hidden {
color: transparent;
background-color: transparent;
}
/* new property button */
#new-property {
display: flex;
Expand Down
67 changes: 67 additions & 0 deletions src/components/validated-text.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { TextComponent } from "obsidian";

type TValidator = (input: string) => string | null;
type TOnValidate = (input: string, err: string[]) => void;

export class ValidatedTextComponent extends TextComponent {
private readonly _validators = new Set<TValidator>();
private readonly _errList: HTMLUListElement;
private _onValidateCb: TOnValidate;
private _earlyReturn: boolean;
private _minLen: number;


constructor(containerEl: HTMLElement) {
super(containerEl);
this._errList = containerEl.createEl("ul");
this._errList.addClasses(["validation-error"]);
this._earlyReturn = true;
this._minLen = 3;
this._onValidateCb = () => { };
this.onChange(() => this.validate());
containerEl.appendChild(this._errList);
}

setValidationErrorClass(className: string) {
this._errList.addClass(className);
return this;
}

setStopOnFirstError(val: boolean = true) {
this._earlyReturn = val;
return this;
}

setMinimumLength(val: number = 3) {
this._minLen = val;
return this;
}

addValidator(fn: TValidator) {
this._validators.add(fn);
return this;
}

onValidate(fn: TOnValidate) {
this._onValidateCb = fn;
return this;
}

validate() {
const input = this.getValue() || "";
const errs: string[] = [];

if (input.length >= this._minLen) {
for (const fn of this._validators) {
const err = fn(input);
if (err !== null)
errs.push(err);
if (errs.length > 0 && this._earlyReturn)
break;
}
}

this._errList.replaceChildren(...errs);
this._onValidateCb(input, errs);
}
}
5 changes: 5 additions & 0 deletions src/const.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { createFrontMatterPropSettings, createFrontMatterProps } from "src/frontmatter";
import type { IFrontMatterPropDefault, ISettings, TFrontMatterPropDefaults } from "./types";

export const KNOWN_BROKEN_DOMAINS = new Map<string, string|null>([
["fastcompany.com", "Fast Company prevents programs like Slurp from accessing their articles."],
["sparksoftcorp.com", null],
]);

export const FRONT_MATTER_ITEM_DEFAULTS: TFrontMatterPropDefaults = new Map<string, IFrontMatterPropDefault>([
{
id: "link", defaultIdx: 0, defaultKey: "link",
Expand Down
15 changes: 8 additions & 7 deletions src/lib/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ export class Logger {
this.vault = plugin.app.vault;
if (plugin.settings && Object.keys(plugin.settings).contains("logs")) {
this.settings = plugin.settings.logs;
} else
} else {
this.settings = { debug: true, logPath: DEFAULT_SETTINGS.logs.logPath };

}

if (this.settings.debug)
plugin.registerInterval(window.setInterval(
() => this.flush(), 500));
Expand Down Expand Up @@ -89,11 +90,11 @@ export class Logger {
const msg = this.buffer[idx];
const optJson = [];
for (const i of msg.optionalParams || []) {
try{
optJson.push(JSON.stringify(serialize(i), undefined, 2));
} catch (err) {
optJson.push(`Unable to stringify: ${i}`);
}
try {
optJson.push(JSON.stringify(serialize(i), undefined, 2));
} catch (err) {
optJson.push(`Unable to stringify: ${i}`);
}
}
content += `##### ${msg.timestamp} | ${msg.level.padStart(5).toUpperCase()} | ${msg.msg}\n` +
`- Caller: \`${msg.caller}\`\n\n`;
Expand Down
33 changes: 25 additions & 8 deletions src/lib/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ export const serialize = (val: unknown) => {
};

// murmurhash3 is simple, fast, and doesn't have external dependencies.
export function murmurhash3_32(key: string, seed: number = 0) {
export const murmurhash3_32 = (key: string, seed: number = 0) => {
var remainder, bytes, h1, h1b, c1, c2, k1, i;

// Initialize the variables
remainder = key.length & 3; // key.length % 4
bytes = key.length - remainder;
Expand All @@ -72,7 +72,7 @@ export function murmurhash3_32(key: string, seed: number = 0) {
// Process the input data in 4-byte chunks
while (i < bytes) {
// extract the next 4 bytes and combine into a 32 bit int
k1 =
k1 =
((key.charCodeAt(i) & 0xff)) |
((key.charCodeAt(++i) & 0xff) << 8) |
((key.charCodeAt(++i) & 0xff) << 16) |
Expand Down Expand Up @@ -103,10 +103,10 @@ export function murmurhash3_32(key: string, seed: number = 0) {
case 3: k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16;
case 2: k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8;
case 1: k1 ^= (key.charCodeAt(i) & 0xff);
k1 = (((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff;
k1 = (k1 << 15) | (k1 >>> 17);
k1 = (((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff;
h1 ^= k1;
k1 = (((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff;
k1 = (k1 << 15) | (k1 >>> 17);
k1 = (((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff;
h1 ^= k1;
}

// Finalize the hash value
Expand All @@ -121,4 +121,21 @@ export function murmurhash3_32(key: string, seed: number = 0) {

// Return the resulting hash as an unsigned 32-bit integer
return h1 >>> 0;
}
};

export const extractDomain = (u: string) => {
// naively strap a proto in case it doesn't have one
const url = (u.split(":").length == 1) ? `https://${u}` : u;
try {
const urlObj = new URL(url);
if (urlObj.protocol === "http:" || urlObj.protocol === "https:") {
const parts = urlObj.host.split('.');
const domain = parts[parts.length - 2] + '.' + parts[parts.length - 1];
if (!domain.startsWith(".") && !domain.endsWith(".")) return domain;
}
} catch (err) {
// returning null after this anyway...
}

return null;
};
80 changes: 48 additions & 32 deletions src/modals/new-note.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,70 @@
import type SlurpPlugin from "main";
import { Modal, App, TextComponent, ProgressBarComponent, Setting } from "obsidian";
import { App, ButtonComponent, Modal, Setting } from "obsidian";
import { BouncingProgressBarComponent } from "../components/bouncing-progress-bar";
import { ValidatedTextComponent } from "../components/validated-text";
import { KNOWN_BROKEN_DOMAINS } from "../const";
import { extractDomain } from "../lib/util";

export class SlurpNewNoteModal extends Modal {
plugin: SlurpPlugin;
url: string;
private readonly plugin: SlurpPlugin;
private readonly WARNING_CLS = "validation";
private readonly URL_FORMAT_ERR = "Invalid URL format.";

constructor(app: App, plugin: SlurpPlugin) {
super(app);
this.plugin = plugin;
this.url = "";
}

onOpen() {
const { contentEl } = this;
private validateKnownBrokenDomains(url: string) {
const domain = extractDomain(url) || "";
const defaultReason = "This site is known to be incompatible with Slurp.";

contentEl.createEl("h3", { text: "What would you like to slurp today?" })
return KNOWN_BROKEN_DOMAINS.has(domain)
? KNOWN_BROKEN_DOMAINS.get(domain) || defaultReason
: null;
}

const urlField = new TextComponent(contentEl)
.setPlaceholder("URL")
.onChange((val) => this.url = val);
urlField.inputEl.setCssProps({ "width": "100%" });
private validateUrlFormat(url: string) {
return extractDomain(url) === null ? this.URL_FORMAT_ERR : null;
}

const progressBar = new ProgressBarComponent(contentEl)
progressBar.disabled = true;
progressBar.setValue(0);
onOpen() {
const { contentEl } = this;
let slurpBtn: ButtonComponent;

const doSlurp = async () => {
urlField.setDisabled(true);
progressBar.setDisabled(false);
let progressIncrement = 1;
new Setting(contentEl)
.setName("What would you like to slurp today?")
.setHeading();

const t = setInterval(() => {
const cur = progressBar.getValue();
if (cur == 100) progressIncrement *= -1;
progressBar.setValue(cur + progressIncrement);
}, 10)
const urlField = new ValidatedTextComponent(contentEl)
.setPlaceholder("https://www.somesite.com/...")
.setMinimumLength(5)
.addValidator((url: string) => this.validateUrlFormat(url))
.addValidator((url: string) => this.validateKnownBrokenDomains(url))
.onValidate((url: string, errs: string[]) => {
slurpBtn.setDisabled(errs.length > 0 || urlField.getValue().length < 5);
});

urlField.inputEl.setCssProps({ "width": "100%" });

try {
this.plugin.slurp(this.url);
} catch (err) { this.plugin.displayError(err as Error); }
const progressBar = new BouncingProgressBarComponent(contentEl);

clearInterval(t);
const doSlurp = () => {
progressBar.start();
this.plugin.slurp(urlField.getValue());
progressBar.stop()
this.close();
};
}

new Setting(contentEl)
.addButton((btn) => btn
.setButtonText("Slurp")
.setCta()
.onClick(doSlurp))
.addButton((btn) => {
btn.setButtonText("Slurp")
.setCta()
.setDisabled(true)
.onClick(doSlurp);
slurpBtn = btn;
return slurpBtn;
});

contentEl.addEventListener("keypress", (k) => (k.key === "Enter") && doSlurp());
}
Expand Down
16 changes: 16 additions & 0 deletions styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,19 @@ available in the app when your plugin is enabled.
If your plugin does not need CSS, delete this file.
*/

.validation-error {
color: var(--text-error);
font-size: small;
}

.validation-error.hidden {
color: transparent;
background-color: transparent;
}

ul.validation-error {
list-style: none;
padding: 0;
margin: 0.5em 0.1em;
}

0 comments on commit c6671c5

Please sign in to comment.