(
210 | ".callout.is-collapsible.is-collapsed"
211 | );
212 | for (let i = 0; i < admonitions.length; i++) {
213 | let admonition = admonitions[i];
214 |
215 | this.calloutManager.collapse(admonition);
216 | }
217 | }
218 | });
219 | this.addCommand({
220 | id: "insert-admonition",
221 | name: "Insert Admonition",
222 | editorCallback: (editor, view) => {
223 | let suggestor = new InsertAdmonitionModal(this);
224 | suggestor.onClose = () => {
225 | if (!suggestor.insert) return;
226 | let titleLine = "",
227 | collapseLine = "";
228 | if (
229 | suggestor.title.length &&
230 | suggestor.title.toLowerCase() !=
231 | suggestor.type.toLowerCase()
232 | ) {
233 | titleLine = `title: ${suggestor.title}\n`;
234 | }
235 | if (
236 | (this.data.autoCollapse &&
237 | suggestor.collapse !=
238 | this.data.defaultCollapseType) ||
239 | (!this.data.autoCollapse &&
240 | suggestor.collapse != "none")
241 | ) {
242 | collapseLine = `collapse: ${suggestor.collapse}\n`;
243 | }
244 | editor.getDoc().replaceSelection(
245 | `\`\`\`ad-${
246 | suggestor.type
247 | }\n${titleLine}${collapseLine}
248 | ${editor.getDoc().getSelection()}
249 | \`\`\`\n`
250 | );
251 | const cursor = editor.getCursor();
252 | editor.setCursor(cursor.line - 3);
253 | };
254 | suggestor.open();
255 | }
256 | });
257 | this.addCommand({
258 | id: "insert-callout",
259 | name: "Insert Callout",
260 | editorCallback: (editor, view) => {
261 | let suggestor = new InsertAdmonitionModal(this);
262 | suggestor.onClose = () => {
263 | if (!suggestor.insert) return;
264 | let title = "",
265 | collapse = "";
266 | if (
267 | (this.data.autoCollapse &&
268 | suggestor.collapse !=
269 | this.data.defaultCollapseType) ||
270 | (!this.data.autoCollapse &&
271 | suggestor.collapse != "none")
272 | ) {
273 | switch (suggestor.collapse) {
274 | case "open": {
275 | collapse = "+";
276 | break;
277 | }
278 | case "closed": {
279 | collapse = "-";
280 | break;
281 | }
282 | }
283 | }
284 | if (
285 | suggestor.title.length &&
286 | suggestor.title.toLowerCase() !=
287 | suggestor.type.toLowerCase()
288 | ) {
289 | title = ` ${suggestor.title}`;
290 | }
291 | const selection = editor.getDoc().getSelection();
292 | editor.getDoc().replaceSelection(
293 | `> [!${suggestor.type}]${collapse}${title}
294 | > ${selection.split("\n").join("\n> ")}
295 | `
296 | );
297 | };
298 | suggestor.open();
299 | }
300 | });
301 | });
302 | }
303 | async downloadIcon(pack: DownloadableIconPack) {
304 | this.iconManager.downloadIcon(pack);
305 | }
306 |
307 | async removeIcon(pack: DownloadableIconPack) {
308 | this.iconManager.removeIcon(pack);
309 | }
310 |
311 | async postprocessor(
312 | type: string,
313 | src: string,
314 | el: HTMLElement,
315 | ctx?: MarkdownPostProcessorContext
316 | ) {
317 | if (!this.admonitions[type]) {
318 | return;
319 | }
320 | try {
321 | const sourcePath =
322 | typeof ctx == "string"
323 | ? ctx
324 | : ctx?.sourcePath ??
325 | this.app.workspace.getActiveFile()?.path ??
326 | "";
327 | let { title, collapse, content, icon, color, metadata } =
328 | getParametersFromSource(type, src, this.admonitions[type]);
329 |
330 | if (this.data.autoCollapse && !collapse) {
331 | collapse = this.data.defaultCollapseType ?? "open";
332 | } else if (collapse && collapse.trim() === "none") {
333 | collapse = "";
334 | }
335 |
336 | /* const iconNode = icon ? this.admonitions[type].icon; */
337 | const admonition = this.admonitions[type];
338 | let admonitionElement = this.getAdmonitionElement(
339 | type,
340 | title,
341 | this.iconManager.iconDefinitions.find(
342 | ({ name }) => icon === name
343 | ) ?? admonition.icon,
344 | color ??
345 | (admonition.injectColor ?? this.data.injectColor
346 | ? admonition.color
347 | : null),
348 | collapse,
349 | sourcePath,
350 | metadata
351 | );
352 | this.renderAdmonitionContent(
353 | admonitionElement,
354 | type,
355 | content,
356 | ctx,
357 | sourcePath,
358 | src
359 | );
360 | if (collapse && collapse != "none") {
361 | this.calloutManager.setCollapsible(admonitionElement);
362 | }
363 | /**
364 | * Replace the tag with the new admonition.
365 | */
366 | const parent = el.parentElement;
367 | if (parent) {
368 | parent.addClass(
369 | "admonition-parent",
370 | `admonition-${type}-parent`
371 | );
372 | }
373 | el.replaceWith(admonitionElement);
374 |
375 | const view = app.workspace.getActiveViewOfType(MarkdownView);
376 | if (view?.editor?.cm?.state?.field(editorLivePreviewField)) {
377 | const editor = view.editor.cm;
378 | admonitionElement.onClickEvent((ev) => {
379 | if (ev.defaultPrevented || ev.detail > 1 || ev.shiftKey)
380 | return;
381 | try {
382 | setTimeout(() => {
383 | try {
384 | const pos = editor.posAtDOM(admonitionElement);
385 | editor.focus();
386 | editor.dispatch({
387 | selection: {
388 | head: pos,
389 | anchor: pos
390 | }
391 | });
392 | } catch (e) {}
393 | }, 10);
394 | } catch (e) {}
395 | });
396 | }
397 |
398 | return admonitionElement;
399 | } catch (e) {
400 | console.error(e);
401 | const pre = createEl("pre");
402 |
403 | pre.createEl("code", {
404 | attr: {
405 | style: `color: var(--text-error) !important`
406 | }
407 | }).createSpan({
408 | text:
409 | "There was an error rendering the admonition:" +
410 | "\n\n" +
411 | src
412 | });
413 |
414 | el.replaceWith(pre);
415 | }
416 | }
417 |
418 | /**
419 | * .callout.admonition.is-collapsible.is-collapsed
420 | * .callout-title
421 | * .callout-icon
422 | * .callout-title-inner
423 | * .callout-fold
424 | * .callout-content
425 | */
426 | getAdmonitionElement(
427 | type: string,
428 | title: string,
429 | icon: AdmonitionIconDefinition,
430 | color?: string,
431 | collapse?: string,
432 | source?: string,
433 | metadata?: string
434 | ): HTMLElement {
435 | const admonition = createDiv({
436 | cls: `callout admonition admonition-${type} admonition-plugin ${
437 | !title?.trim().length ? "no-title" : ""
438 | }`,
439 | attr: {
440 | style: color ? `--callout-color: ${color};` : "",
441 | "data-callout": type,
442 | "data-callout-fold": "",
443 | "data-callout-metadata": metadata ?? ""
444 | }
445 | });
446 | const titleEl = admonition.createDiv({
447 | cls: `callout-title admonition-title ${
448 | !title?.trim().length ? "no-title" : ""
449 | }`
450 | });
451 |
452 | if (title && title.trim().length) {
453 | //build icon element
454 | const iconEl = titleEl.createDiv(
455 | "callout-icon admonition-title-icon"
456 | );
457 | if (icon && icon.name && icon.type) {
458 | iconEl.appendChild(
459 | this.iconManager.getIconNode(icon) ?? createDiv()
460 | );
461 | }
462 |
463 | //get markdown
464 | const titleInnerEl = titleEl.createDiv(
465 | "callout-title-inner admonition-title-content"
466 | );
467 | MarkdownRenderer.render(
468 | this.app,
469 | title,
470 | titleInnerEl,
471 | source ?? "",
472 | this
473 | );
474 | if (
475 | titleInnerEl.firstElementChild &&
476 | titleInnerEl.firstElementChild instanceof HTMLParagraphElement
477 | ) {
478 | titleInnerEl.setChildrenInPlace(
479 | Array.from(titleInnerEl.firstElementChild.childNodes)
480 | );
481 | }
482 | }
483 |
484 | //add them to title element
485 |
486 | if (collapse) {
487 | admonition.addClass("is-collapsible");
488 | if (collapse == "closed") {
489 | admonition.addClass("is-collapsed");
490 | }
491 | }
492 | if (!this.data.dropShadow) {
493 | admonition.addClass("no-drop");
494 | }
495 | return admonition;
496 | }
497 |
498 | renderAdmonitionContent(
499 | admonitionElement: HTMLElement,
500 | type: string,
501 | content: string,
502 | ctx: MarkdownPostProcessorContext,
503 | sourcePath: string,
504 | src: string
505 | ) {
506 | let markdownRenderChild = new MarkdownRenderChild(admonitionElement);
507 | markdownRenderChild.containerEl = admonitionElement;
508 | if (ctx && !(typeof ctx == "string")) {
509 | ctx.addChild(markdownRenderChild);
510 | }
511 |
512 | if (content && content?.trim().length) {
513 | /**
514 | * Render the content as markdown and append it to the admonition.
515 | */
516 | const contentEl = this.getAdmonitionContentElement(
517 | type,
518 | admonitionElement,
519 | content
520 | );
521 | if (/^`{3,}mermaid/m.test(content)) {
522 | const wasCollapsed = !admonitionElement.hasAttribute("open");
523 | if (admonitionElement instanceof HTMLDetailsElement) {
524 | admonitionElement.setAttribute("open", "open");
525 | }
526 | setImmediate(() => {
527 | MarkdownRenderer.renderMarkdown(
528 | content,
529 | contentEl,
530 | sourcePath,
531 | markdownRenderChild
532 | );
533 | if (
534 | admonitionElement instanceof HTMLDetailsElement &&
535 | wasCollapsed
536 | ) {
537 | admonitionElement.removeAttribute("open");
538 | }
539 | });
540 | } else {
541 | MarkdownRenderer.renderMarkdown(
542 | content,
543 | contentEl,
544 | sourcePath,
545 | markdownRenderChild
546 | );
547 | }
548 |
549 | if (
550 | (!content.length || contentEl.textContent.trim() == "") &&
551 | this.data.hideEmpty
552 | )
553 | admonitionElement.addClass("no-content");
554 |
555 | const taskLists = contentEl.querySelectorAll(
556 | ".task-list-item-checkbox"
557 | );
558 | if (taskLists?.length) {
559 | const split = src.split("\n");
560 | let slicer = 0;
561 | taskLists.forEach((task) => {
562 | const line = split
563 | .slice(slicer)
564 | .findIndex((l) => /^[ \t>]*\- \[.\]/.test(l));
565 |
566 | if (line == -1) return;
567 | task.dataset.line = `${line + slicer + 1}`;
568 | slicer = line + slicer + 1;
569 | });
570 | }
571 | }
572 | }
573 |
574 | getAdmonitionContentElement(
575 | type: string,
576 | admonitionElement: HTMLElement,
577 | content: string
578 | ) {
579 | const contentEl = admonitionElement.createDiv(
580 | "callout-content admonition-content"
581 | );
582 | if (this.admonitions[type].copy ?? this.data.copyButton) {
583 | let copy = contentEl.createDiv("admonition-content-copy");
584 | setIcon(copy, "copy");
585 | copy.addEventListener("click", () => {
586 | navigator.clipboard.writeText(content.trim()).then(async () => {
587 | new Notice("Admonition content copied to clipboard.");
588 | });
589 | });
590 | }
591 | return contentEl;
592 | }
593 |
594 | registerType(type: string) {
595 | /** Turn on CodeMirror syntax highlighting for this "language" */
596 | if (this.data.syntaxHighlight) {
597 | this.turnOnSyntaxHighlighting([type]);
598 | }
599 |
600 | /** Register an admonition code-block post processor for legacy support. */
601 | if (this.postprocessors.has(type)) {
602 | MarkdownPreviewRenderer.unregisterCodeBlockPostProcessor(
603 | `ad-${type}`
604 | );
605 | }
606 | this.postprocessors.set(
607 | type,
608 | this.registerMarkdownCodeBlockProcessor(
609 | `ad-${type}`,
610 | (src, el, ctx) => this.postprocessor(type, src, el, ctx)
611 | )
612 | );
613 | const admonition = this.admonitions[type];
614 | if (admonition.command) {
615 | this.registerCommandsFor(admonition);
616 | }
617 | }
618 | get admonitions() {
619 | return { ...ADMONITION_MAP, ...this.data.userAdmonitions };
620 | }
621 | async addAdmonition(admonition: Admonition): Promise {
622 | this.data.userAdmonitions = {
623 | ...this.data.userAdmonitions,
624 | [admonition.type]: admonition
625 | };
626 |
627 | this.registerType(admonition.type);
628 |
629 | /** Create the admonition type in CSS */
630 | this.calloutManager.addAdmonition(admonition);
631 |
632 | await this.saveSettings();
633 | }
634 | registerCommandsFor(admonition: Admonition) {
635 | admonition.command = true;
636 | this.addCommand({
637 | id: `insert-${admonition.type}-callout`,
638 | name: `Insert ${admonition.type} Callout`,
639 | editorCheckCallback: (checking, editor, view) => {
640 | if (checking) return admonition.command;
641 | if (admonition.command) {
642 | try {
643 | const selection = editor.getDoc().getSelection();
644 | editor.getDoc().replaceSelection(
645 | `> [!${admonition.type}]
646 | > ${selection.split("\n").join("\n> ")}
647 | `
648 | );
649 | const cursor = editor.getCursor();
650 | editor.setCursor(cursor.line - 2);
651 | } catch (e) {
652 | new Notice(
653 | "There was an issue inserting the admonition."
654 | );
655 | }
656 | }
657 | }
658 | });
659 | this.addCommand({
660 | id: `insert-${admonition.type}`,
661 | name: `Insert ${admonition.type}`,
662 | editorCheckCallback: (checking, editor, view) => {
663 | if (checking) return admonition.command;
664 | if (admonition.command) {
665 | try {
666 | editor.getDoc().replaceSelection(
667 | `\`\`\`ad-${admonition.type}
668 |
669 | ${editor.getDoc().getSelection()}
670 |
671 | \`\`\`\n`
672 | );
673 | const cursor = editor.getCursor();
674 | editor.setCursor(cursor.line - 2);
675 | } catch (e) {
676 | new Notice(
677 | "There was an issue inserting the admonition."
678 | );
679 | }
680 | }
681 | }
682 | });
683 | this.addCommand({
684 | id: `insert-${admonition.type}-with-title`,
685 | name: `Insert ${admonition.type} With Title`,
686 | editorCheckCallback: (checking, editor, view) => {
687 | if (checking) return admonition.command;
688 | if (admonition.command) {
689 | try {
690 | const title = admonition.title ?? "";
691 | editor.getDoc().replaceSelection(
692 | `\`\`\`ad-${admonition.type}
693 | title: ${title}
694 |
695 | ${editor.getDoc().getSelection()}
696 |
697 | \`\`\`\n`
698 | );
699 | const cursor = editor.getCursor();
700 | editor.setCursor(cursor.line - 3);
701 | } catch (e) {
702 | new Notice(
703 | "There was an issue inserting the admonition."
704 | );
705 | }
706 | }
707 | }
708 | });
709 | }
710 | unregisterType(admonition: Admonition) {
711 | if (this.data.syntaxHighlight) {
712 | this.turnOffSyntaxHighlighting([admonition.type]);
713 | }
714 |
715 | if (admonition.command) {
716 | this.unregisterCommandsFor(admonition);
717 | }
718 |
719 | if (this.postprocessors.has(admonition.type)) {
720 | MarkdownPreviewRenderer.unregisterPostProcessor(
721 | this.postprocessors.get(admonition.type)
722 | );
723 | MarkdownPreviewRenderer.unregisterCodeBlockPostProcessor(
724 | `ad-${admonition.type}`
725 | );
726 | this.postprocessors.delete(admonition.type);
727 | }
728 | }
729 | async removeAdmonition(admonition: Admonition) {
730 | if (this.data.userAdmonitions[admonition.type]) {
731 | delete this.data.userAdmonitions[admonition.type];
732 | }
733 |
734 | this.unregisterType(admonition);
735 |
736 | /** Remove the admonition type in CSS */
737 | this.calloutManager.removeAdmonition(admonition);
738 |
739 | await this.saveSettings();
740 | }
741 | unregisterCommandsFor(admonition: Admonition) {
742 | admonition.command = false;
743 |
744 | if (
745 | this.app.commands.findCommand(
746 | `obsidian-admonition:insert-${admonition.type}`
747 | )
748 | ) {
749 | delete this.app.commands.editorCommands[
750 | `obsidian-admonition:insert-${admonition.type}`
751 | ];
752 | delete this.app.commands.editorCommands[
753 | `obsidian-admonition:insert-${admonition.type}-with-title`
754 | ];
755 | delete this.app.commands.commands[
756 | `obsidian-admonition:insert-${admonition.type}`
757 | ];
758 | delete this.app.commands.commands[
759 | `obsidian-admonition:insert-${admonition.type}-with-title`
760 | ];
761 | }
762 | }
763 |
764 | async saveSettings() {
765 | this.data.version = this.manifest.version;
766 |
767 | await this.saveData(this.data);
768 | }
769 | async loadSettings() {
770 | const loaded: AdmonitionSettings = await this.loadData();
771 | this.data = Object.assign({}, DEFAULT_APP_SETTINGS, loaded);
772 |
773 | if (this.data.userAdmonitions) {
774 | if (
775 | !this.data.version ||
776 | Number(this.data.version.split(".")[0]) < 5
777 | ) {
778 | for (let admonition in this.data.userAdmonitions) {
779 | if (
780 | Object.prototype.hasOwnProperty.call(
781 | this.data.userAdmonitions[admonition],
782 | "type"
783 | )
784 | )
785 | continue;
786 | this.data.userAdmonitions[admonition] = {
787 | ...this.data.userAdmonitions[admonition],
788 | icon: {
789 | type: "font-awesome",
790 | name: this.data.userAdmonitions[admonition]
791 | .icon as unknown as IconName
792 | }
793 | };
794 | }
795 | }
796 |
797 | if (
798 | !this.data.version ||
799 | Number(this.data.version.split(".")[0]) < 8
800 | ) {
801 | new Notice(
802 | createFragment((e) => {
803 | e.createSpan({
804 | text: "Admonitions: Obsidian now has native support for callouts! Check out the "
805 | });
806 |
807 | e.createEl("a", {
808 | text: "Admonitions ReadMe",
809 | href: "obsidian://show-plugin?id=obsidian-admonition"
810 | });
811 |
812 | e.createSpan({
813 | text: " for what that means for Admonitions going forward."
814 | });
815 | }),
816 | 0
817 | );
818 | }
819 | }
820 |
821 | if (
822 | !this.data.rpgDownloadedOnce &&
823 | this.data.userAdmonitions &&
824 | Object.values(this.data.userAdmonitions).some((admonition) => {
825 | if (admonition.icon.type == "rpg") return true;
826 | }) &&
827 | !this.data.icons.includes("rpg")
828 | ) {
829 | try {
830 | await this.downloadIcon("rpg");
831 | this.data.rpgDownloadedOnce = true;
832 | } catch (e) {}
833 | }
834 |
835 | await this.saveSettings();
836 | }
837 |
838 | turnOnSyntaxHighlighting(types: string[] = Object.keys(this.admonitions)) {
839 | if (!this.data.syntaxHighlight) return;
840 | types.forEach((type) => {
841 | if (this.data.syntaxHighlight) {
842 | /** Process from @deathau's syntax highlight plugin */
843 | const [, cmPatchedType] = `${type}`.match(
844 | /^([\w+#-]*)[^\n`]*$/
845 | );
846 | window.CodeMirror.defineMode(
847 | `ad-${cmPatchedType}`,
848 | (config, options) => {
849 | return window.CodeMirror.getMode({}, "hypermd");
850 | }
851 | );
852 | }
853 | });
854 |
855 | this.app.workspace.onLayoutReady(() =>
856 | this.app.workspace.iterateCodeMirrors((cm) =>
857 | cm.setOption("mode", cm.getOption("mode"))
858 | )
859 | );
860 | }
861 | turnOffSyntaxHighlighting(types: string[] = Object.keys(this.admonitions)) {
862 | types.forEach((type) => {
863 | if (window.CodeMirror.modes.hasOwnProperty(`ad-${type}`)) {
864 | delete window.CodeMirror.modes[`ad-${type}`];
865 | }
866 | });
867 | this.app.workspace.onLayoutReady(() =>
868 | this.app.workspace.iterateCodeMirrors((cm) =>
869 | cm.setOption("mode", cm.getOption("mode"))
870 | )
871 | );
872 | }
873 |
874 | async onunload() {
875 | console.log("Obsidian Admonition unloaded");
876 | this.postprocessors = null;
877 | this.turnOffSyntaxHighlighting();
878 | }
879 | }
880 |
--------------------------------------------------------------------------------
/src/modal/confirm.ts:
--------------------------------------------------------------------------------
1 | import { App, ButtonComponent, Modal } from "obsidian";
2 |
3 | export async function confirmWithModal(
4 | app: App,
5 | text: string,
6 | buttons: { cta: string; secondary: string } = {
7 | cta: "Yes",
8 | secondary: "No"
9 | }
10 | ): Promise {
11 | return new Promise((resolve, reject) => {
12 | try {
13 | const modal = new ConfirmModal(app, text, buttons);
14 | modal.onClose = () => {
15 | resolve(modal.confirmed);
16 | };
17 | modal.open();
18 | } catch (e) {
19 | reject();
20 | }
21 | });
22 | }
23 |
24 | export class ConfirmModal extends Modal {
25 | constructor(
26 | app: App,
27 | public text: string,
28 | public buttons: { cta: string; secondary: string }
29 | ) {
30 | super(app);
31 | }
32 | confirmed: boolean = false;
33 | async display() {
34 | this.contentEl.empty();
35 | this.contentEl.addClass("confirm-modal");
36 | this.contentEl.createEl("p", {
37 | text: this.text
38 | });
39 | const buttonEl = this.contentEl.createDiv(
40 | "fantasy-calendar-confirm-buttons"
41 | );
42 | new ButtonComponent(buttonEl)
43 | .setButtonText(this.buttons.cta)
44 | .setCta()
45 | .onClick(() => {
46 | this.confirmed = true;
47 | this.close();
48 | });
49 | new ButtonComponent(buttonEl)
50 | .setButtonText(this.buttons.secondary)
51 | .onClick(() => {
52 | this.close();
53 | });
54 | }
55 | onOpen() {
56 | this.display();
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/modal/export.ts:
--------------------------------------------------------------------------------
1 | import { Modal, Setting } from "obsidian";
2 | import ObsidianAdmonition from "src/main";
3 |
4 | export default class Export extends Modal {
5 | constructor(public plugin: ObsidianAdmonition) {
6 | super(app);
7 | }
8 | admonitionDefinitions = Object.values(this.plugin.data.userAdmonitions);
9 |
10 | admonitionNames = Object.keys(this.plugin.data.userAdmonitions);
11 |
12 | selectedAdmonitions = [...this.admonitionNames];
13 |
14 | export = false;
15 |
16 | onOpen() {
17 | this.titleEl.setText("Export Admonitions");
18 | this.containerEl.addClasses([
19 | "admonition-settings",
20 | "admonition-modal",
21 | "admonition-export-modal"
22 | ]);
23 | new Setting(this.contentEl).addButton((b) =>
24 | b.setButtonText("Export Selected").onClick(() => {
25 | this.export = true;
26 | this.close();
27 | })
28 | );
29 | let toggleEl: HTMLDivElement;
30 | new Setting(this.contentEl)
31 | .addButton((b) =>
32 | b
33 | .setButtonText("Select All")
34 | .setCta()
35 | .onClick(() => {
36 | this.selectedAdmonitions = [...this.admonitionNames];
37 | this.generateToggles(toggleEl);
38 | })
39 | )
40 | .addButton((b) =>
41 | b.setButtonText("Deselect All").onClick(() => {
42 | this.selectedAdmonitions = [];
43 | this.generateToggles(toggleEl);
44 | })
45 | );
46 | toggleEl = this.contentEl.createDiv("additional");
47 | this.generateToggles(toggleEl);
48 | }
49 |
50 | generateToggles(toggleEl: HTMLDivElement) {
51 | toggleEl.empty();
52 | for (const name of this.admonitionNames) {
53 | new Setting(toggleEl).setName(name).addToggle((t) => {
54 | t.setValue(this.selectedAdmonitions.includes(name)).onChange(
55 | (v) => {
56 | if (v) {
57 | this.selectedAdmonitions.push(name);
58 | } else {
59 | this.selectedAdmonitions.remove(name);
60 | }
61 | }
62 | );
63 | });
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/modal/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | FuzzyMatch,
3 | Modal,
4 | Notice,
5 | SearchComponent,
6 | Setting,
7 | TextComponent,
8 | renderMatches,
9 | setIcon
10 | } from "obsidian";
11 |
12 | import { FuzzyInputSuggest } from "@javalent/utilities";
13 |
14 | import { Admonition, AdmonitionIconDefinition } from "src/@types";
15 | import ObsidianAdmonition from "src/main";
16 |
17 | export class IconSuggestionModal extends FuzzyInputSuggest {
18 | constructor(
19 | public plugin: ObsidianAdmonition,
20 | input: TextComponent | SearchComponent,
21 | items: AdmonitionIconDefinition[]
22 | ) {
23 | super(plugin.app, input, items);
24 | }
25 | renderNote(
26 | noteEL: HTMLElement,
27 | result: FuzzyMatch
28 | ): void {
29 | noteEL.setText(this.plugin.iconManager.getIconModuleName(result.item));
30 | }
31 | renderTitle(
32 | titleEl: HTMLElement,
33 | result: FuzzyMatch
34 | ): void {
35 | renderMatches(titleEl, result.item.name, result.match.matches);
36 | }
37 | renderFlair(
38 | flairEl: HTMLElement,
39 | result: FuzzyMatch
40 | ): void {
41 | const { item } = result;
42 |
43 | flairEl.appendChild(
44 | this.plugin.iconManager.getIconNode(item) ?? createDiv()
45 | );
46 | }
47 |
48 | getItemText(item: AdmonitionIconDefinition) {
49 | return item.name;
50 | }
51 | }
52 | class AdmonitionSuggestionModal extends FuzzyInputSuggest {
53 | constructor(
54 | public plugin: ObsidianAdmonition,
55 | input: TextComponent | SearchComponent,
56 | items: Admonition[]
57 | ) {
58 | super(plugin.app, input, items);
59 | }
60 | renderTitle(titleEl: HTMLElement, result: FuzzyMatch): void {
61 | renderMatches(titleEl, result.item.type, result.match.matches);
62 | }
63 | renderFlair(flairEl: HTMLElement, result: FuzzyMatch): void {
64 | const { item } = result;
65 | flairEl
66 | .appendChild(
67 | this.plugin.iconManager.getIconNode(item.icon) ?? createDiv()
68 | )
69 | .setAttribute("color", `rgb(${item.color})`);
70 | }
71 | getItemText(item: Admonition) {
72 | return item.type;
73 | }
74 | }
75 |
76 | export class InsertAdmonitionModal extends Modal {
77 | public type: string;
78 | public title: string;
79 | public noTitle: boolean;
80 | public collapse: "open" | "closed" | "none" = this.plugin.data.autoCollapse
81 | ? this.plugin.data.defaultCollapseType
82 | : "none";
83 | private element: HTMLElement;
84 | admonitionEl: HTMLDivElement;
85 | insert: boolean;
86 | constructor(private plugin: ObsidianAdmonition) {
87 | super(plugin.app);
88 |
89 | this.containerEl.addClass("insert-admonition-modal");
90 |
91 | this.onOpen = () => this.display(true);
92 | }
93 | private async display(focus?: boolean) {
94 | const { contentEl } = this;
95 |
96 | contentEl.empty();
97 |
98 | const typeSetting = new Setting(contentEl);
99 | typeSetting.setName("Admonition Type").addText((t) => {
100 | t.setPlaceholder("Admonition Type").setValue(this.type);
101 | const modal = new AdmonitionSuggestionModal(
102 | this.plugin,
103 | t,
104 | this.plugin.admonitionArray
105 | );
106 |
107 | const build = () => {
108 | if (
109 | t.inputEl.value &&
110 | this.plugin.admonitions[t.inputEl.value]
111 | ) {
112 | this.type = t.inputEl.value;
113 | this.title = this.plugin.admonitions[this.type].title;
114 | if (!this.title?.length) {
115 | this.title =
116 | this.type[0].toUpperCase() +
117 | this.type.slice(1).toLowerCase();
118 | }
119 | titleInput.setValue(this.title);
120 | } else {
121 | new Notice("No admonition type by that name exists.");
122 | t.inputEl.value = "";
123 | }
124 |
125 | this.buildAdmonition();
126 | };
127 |
128 | t.inputEl.onblur = build;
129 |
130 | modal.onSelect((item) => {
131 | t.inputEl.value = item.item.type;
132 | build();
133 | modal.close();
134 | });
135 | });
136 |
137 | let titleInput: TextComponent;
138 |
139 | const titleSetting = new Setting(contentEl);
140 | titleSetting
141 | .setName("Admonition Title")
142 | .setDesc("Leave blank to render without a title.")
143 | .addText((t) => {
144 | titleInput = t;
145 | t.setValue(this.title);
146 |
147 | t.onChange((v) => {
148 | this.title = v;
149 | if (v.length == 0) {
150 | this.noTitle = true;
151 | } else {
152 | this.noTitle = false;
153 | }
154 | if (this.element) {
155 | const admonition = this.plugin.admonitions[this.type];
156 | const element = this.plugin.getAdmonitionElement(
157 | this.type,
158 | this.title,
159 | admonition.icon,
160 | admonition.injectColor ??
161 | this.plugin.data.injectColor
162 | ? admonition.color
163 | : null,
164 | this.collapse
165 | );
166 | element.createDiv({
167 | cls: "admonition-content",
168 | text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla."
169 | });
170 | this.element.replaceWith(element);
171 | this.element = element;
172 | }
173 | });
174 | });
175 |
176 | const collapseSetting = new Setting(contentEl);
177 | collapseSetting.setName("Make Collapsible").addDropdown((d) => {
178 | d.addOption("open", "Open");
179 | d.addOption("closed", "Closed");
180 | d.addOption("none", "None");
181 | d.setValue(this.collapse);
182 | d.onChange((v: "open" | "closed" | "none") => {
183 | this.collapse = v;
184 | this.buildAdmonition();
185 | });
186 | });
187 |
188 | this.admonitionEl = this.contentEl.createDiv();
189 | this.buildAdmonition();
190 |
191 | new Setting(contentEl)
192 | .addButton((b) =>
193 | b
194 | .setButtonText("Insert")
195 | .setCta()
196 | .onClick(() => {
197 | this.insert = true;
198 | this.close();
199 | })
200 | )
201 | .addExtraButton((b) => {
202 | b.setIcon("cross")
203 | .setTooltip("Cancel")
204 | .onClick(() => this.close());
205 | b.extraSettingsEl.setAttr("tabindex", 0);
206 | b.extraSettingsEl.onkeydown = (evt) => {
207 | evt.key == "Enter" && this.close();
208 | };
209 | });
210 | }
211 | buildAdmonition() {
212 | this.admonitionEl.empty();
213 | if (this.type && this.plugin.admonitions[this.type]) {
214 | const admonition = this.plugin.admonitions[this.type];
215 | this.element = this.plugin.getAdmonitionElement(
216 | this.type,
217 | this.title,
218 | admonition.icon,
219 | admonition.injectColor ?? this.plugin.data.injectColor
220 | ? admonition.color
221 | : null,
222 | this.collapse
223 | );
224 | this.element.createDiv({
225 | cls: "admonition-content",
226 | text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla."
227 | });
228 | this.admonitionEl.appendChild(this.element);
229 | }
230 | }
231 | }
232 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | @import "./assets/main.scss";
2 | @import "./assets/callout.scss";
3 |
--------------------------------------------------------------------------------
/src/suggest/suggest.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Editor,
3 | EditorPosition,
4 | EditorSuggest,
5 | EditorSuggestContext,
6 | EditorSuggestTriggerInfo
7 | } from "obsidian";
8 | import { Admonition } from "src/@types";
9 | import ObsidianAdmonition from "src/main";
10 |
11 | abstract class AdmonitionOrCalloutSuggester extends EditorSuggest<
12 | [string, Admonition]
13 | > {
14 | constructor(public plugin: ObsidianAdmonition) {
15 | super(plugin.app);
16 | }
17 | getSuggestions(ctx: EditorSuggestContext) {
18 | if (!ctx.query?.length) return Object.entries(this.plugin.admonitions);
19 |
20 | return Object.entries(this.plugin.admonitions).filter((p) =>
21 | p[0].toLowerCase().contains(ctx.query.toLowerCase())
22 | );
23 | }
24 | renderSuggestion(
25 | [text, item]: [text: string, item: Admonition],
26 | el: HTMLElement
27 | ) {
28 | el.addClasses(["admonition-suggester-item", "mod-complex"]);
29 | el.style.setProperty("--callout-color", item.color);
30 | el.createSpan({ text });
31 | const iconDiv = el.createDiv("suggestion-aux").createDiv({
32 | cls: "suggestion-flair",
33 | attr: {
34 | style: `color: rgb(var(--callout-color))`
35 | }
36 | });
37 | let iconEl = this.plugin.iconManager.getIconNode(item.icon);
38 | // Unpack the icon if it's an Obsidian one, as they're wrapped with an extra
39 | if (iconEl instanceof HTMLDivElement && iconEl.childElementCount == 1)
40 | iconEl = iconEl.firstElementChild;
41 | else if (iconEl !== null) {
42 | iconEl.removeClass("svg-inline--fa");
43 | iconEl.addClass("svg-icon");
44 | }
45 | iconDiv.appendChild(iconEl ?? createDiv());
46 | }
47 | onTrigger(
48 | cursor: EditorPosition,
49 | editor: Editor
50 | ): EditorSuggestTriggerInfo {
51 | const line = editor.getLine(cursor.line);
52 | const match = this.testAndReturnQuery(line, cursor);
53 | if (!match) return null;
54 | const [_, query] = match;
55 |
56 | if (
57 | Object.keys(this.plugin.admonitions).find(
58 | (p) => p.toLowerCase() == query.toLowerCase()
59 | )
60 | ) {
61 | return null;
62 | }
63 |
64 | return {
65 | end: cursor,
66 | start: {
67 | ch: match.index + this.offset,
68 | line: cursor.line
69 | },
70 | query
71 | };
72 | }
73 | abstract offset: number;
74 | abstract selectSuggestion(
75 | value: [string, Admonition],
76 | evt: MouseEvent | KeyboardEvent
77 | ): void;
78 | abstract testAndReturnQuery(
79 | line: string,
80 | cursor: EditorPosition
81 | ): RegExpMatchArray | null;
82 | }
83 |
84 | export class CalloutSuggest extends AdmonitionOrCalloutSuggester {
85 | offset = 4;
86 | selectSuggestion(
87 | [text]: [text: string, item: Admonition],
88 | evt: MouseEvent | KeyboardEvent
89 | ): void {
90 | if (!this.context) return;
91 |
92 | const line = this.context.editor
93 | .getLine(this.context.end.line)
94 | .slice(this.context.end.ch);
95 | const [_, exists] = line.match(/^(\] ?)/) ?? [];
96 |
97 | this.context.editor.replaceRange(
98 | `${text}] `,
99 | this.context.start,
100 | {
101 | ...this.context.end,
102 | ch:
103 | this.context.start.ch +
104 | this.context.query.length +
105 | (exists?.length ?? 0)
106 | },
107 | "admonitions"
108 | );
109 |
110 | this.context.editor.setCursor(
111 | this.context.start.line,
112 | this.context.start.ch + text.length + 2
113 | );
114 |
115 | this.close();
116 | }
117 | testAndReturnQuery(
118 | line: string,
119 | cursor: EditorPosition
120 | ): RegExpMatchArray | null {
121 | if (/> ?\[!\w+\]/.test(line.slice(0, cursor.ch))) return null;
122 | if (!/> ?\[!\w*/.test(line)) return null;
123 | return line.match(/> ?\[!(\w*)\]?/);
124 | }
125 | }
126 | export class AdmonitionSuggest extends AdmonitionOrCalloutSuggester {
127 | offset = 6;
128 | selectSuggestion(
129 | [text]: [text: string, item: Admonition],
130 | evt: MouseEvent | KeyboardEvent
131 | ): void {
132 | if (!this.context) return;
133 |
134 | this.context.editor.replaceRange(
135 | `${text}`,
136 | this.context.start,
137 | this.context.end,
138 | "admonitions"
139 | );
140 |
141 | this.close();
142 | }
143 | testAndReturnQuery(
144 | line: string,
145 | cursor: EditorPosition
146 | ): RegExpMatchArray | null {
147 | if (!/```ad-\w*/.test(line)) return null;
148 | return line.match(/```ad-(\w*)/);
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/src/util/constants.ts:
--------------------------------------------------------------------------------
1 | import { Admonition } from "../@types";
2 |
3 | export const ADD_ADMONITION_COMMAND_ICON = `
`;
4 | export const ADD_COMMAND_NAME = "admonition-add-command";
5 |
6 | export const REMOVE_ADMONITION_COMMAND_ICON = `
`;
7 | export const REMOVE_COMMAND_NAME = "admonition-remove-command";
8 |
9 | export const WARNING_ICON = `
`;
10 | export const WARNING_ICON_NAME = "admonition-warning";
11 |
12 | export const SPIN_ICON = `
`;
13 | export const SPIN_ICON_NAME = "admonition-spin";
14 |
15 | export const ADMONITION_MAP: Record
= {
16 | note: {
17 | type: "note",
18 | color: "68, 138, 255",
19 | icon: {
20 | type: "font-awesome",
21 | name: "pencil-alt"
22 | },
23 | command: false,
24 | noTitle: false
25 | },
26 | seealso: {
27 | type: "note",
28 | color: "68, 138, 255",
29 | icon: {
30 | type: "font-awesome",
31 | name: "pencil-alt"
32 | },
33 | command: false,
34 | noTitle: false
35 | },
36 | abstract: {
37 | type: "abstract",
38 | color: "0, 176, 255",
39 | icon: {
40 | type: "font-awesome",
41 | name: "book"
42 | },
43 | command: false,
44 | noTitle: false
45 | },
46 | summary: {
47 | type: "abstract",
48 | color: "0, 176, 255",
49 | icon: {
50 | type: "font-awesome",
51 | name: "book"
52 | },
53 | command: false,
54 | noTitle: false
55 | },
56 | tldr: {
57 | type: "abstract",
58 | color: "0, 176, 255",
59 | icon: {
60 | type: "font-awesome",
61 | name: "book"
62 | },
63 | command: false,
64 | noTitle: false
65 | },
66 | info: {
67 | type: "info",
68 | color: "0, 184, 212",
69 | icon: {
70 | type: "font-awesome",
71 | name: "info-circle"
72 | },
73 | command: false,
74 | noTitle: false
75 | },
76 | todo: {
77 | type: "info",
78 | color: "0, 184, 212",
79 | icon: {
80 | type: "font-awesome",
81 | name: "info-circle"
82 | },
83 | command: false,
84 | noTitle: false
85 | },
86 | tip: {
87 | type: "tip",
88 | color: "0, 191, 165",
89 | icon: {
90 | type: "font-awesome",
91 | name: "fire"
92 | },
93 | command: false,
94 | noTitle: false
95 | },
96 | hint: {
97 | type: "tip",
98 | color: "0, 191, 165",
99 | icon: {
100 | type: "font-awesome",
101 | name: "fire"
102 | },
103 | command: false,
104 | noTitle: false
105 | },
106 | important: {
107 | type: "tip",
108 | color: "0, 191, 165",
109 | icon: {
110 | type: "font-awesome",
111 | name: "fire"
112 | },
113 | command: false,
114 | noTitle: false
115 | },
116 | success: {
117 | type: "success",
118 | color: "0, 200, 83",
119 | icon: {
120 | type: "font-awesome",
121 | name: "check-circle"
122 | },
123 | command: false,
124 | noTitle: false
125 | },
126 | check: {
127 | type: "success",
128 | color: "0, 200, 83",
129 | icon: {
130 | type: "font-awesome",
131 | name: "check-circle"
132 | },
133 | command: false,
134 | noTitle: false
135 | },
136 | done: {
137 | type: "success",
138 | color: "0, 200, 83",
139 | icon: {
140 | type: "font-awesome",
141 | name: "check-circle"
142 | },
143 | command: false,
144 | noTitle: false
145 | },
146 | question: {
147 | type: "question",
148 | color: "100, 221, 23",
149 | icon: {
150 | type: "font-awesome",
151 | name: "question-circle"
152 | },
153 | command: false,
154 | noTitle: false
155 | },
156 | help: {
157 | type: "question",
158 | color: "100, 221, 23",
159 | icon: {
160 | type: "font-awesome",
161 | name: "question-circle"
162 | },
163 | command: false,
164 | noTitle: false
165 | },
166 | faq: {
167 | type: "question",
168 | color: "100, 221, 23",
169 | icon: {
170 | type: "font-awesome",
171 | name: "question-circle"
172 | },
173 | command: false,
174 | noTitle: false
175 | },
176 | warning: {
177 | type: "warning",
178 | color: "255, 145, 0",
179 | icon: {
180 | type: "font-awesome",
181 | name: "exclamation-triangle"
182 | },
183 | command: false,
184 | noTitle: false
185 | },
186 | caution: {
187 | type: "warning",
188 | color: "255, 145, 0",
189 | icon: {
190 | type: "font-awesome",
191 | name: "exclamation-triangle"
192 | },
193 | command: false,
194 | noTitle: false
195 | },
196 | attention: {
197 | type: "warning",
198 | color: "255, 145, 0",
199 | icon: {
200 | type: "font-awesome",
201 | name: "exclamation-triangle"
202 | },
203 | command: false,
204 | noTitle: false
205 | },
206 | failure: {
207 | type: "failure",
208 | color: "255, 82, 82",
209 | icon: {
210 | type: "font-awesome",
211 | name: "times-circle"
212 | },
213 | command: false,
214 | noTitle: false
215 | },
216 | fail: {
217 | type: "failure",
218 | color: "255, 82, 82",
219 | icon: {
220 | type: "font-awesome",
221 | name: "times-circle"
222 | },
223 | command: false,
224 | noTitle: false
225 | },
226 | missing: {
227 | type: "failure",
228 | color: "255, 82, 82",
229 | icon: {
230 | type: "font-awesome",
231 | name: "times-circle"
232 | },
233 | command: false,
234 | noTitle: false
235 | },
236 | danger: {
237 | type: "danger",
238 | color: "255, 23, 68",
239 | icon: {
240 | type: "font-awesome",
241 | name: "bolt"
242 | },
243 | command: false,
244 | noTitle: false
245 | },
246 | error: {
247 | type: "danger",
248 | color: "255, 23, 68",
249 | icon: {
250 | type: "font-awesome",
251 | name: "bolt"
252 | },
253 | command: false,
254 | noTitle: false
255 | },
256 | bug: {
257 | type: "bug",
258 | color: "245, 0, 87",
259 | icon: {
260 | type: "font-awesome",
261 | name: "bug"
262 | },
263 | command: false,
264 | noTitle: false
265 | },
266 | example: {
267 | type: "example",
268 | color: "124, 77, 255",
269 | icon: {
270 | type: "font-awesome",
271 | name: "list-ol"
272 | },
273 | command: false,
274 | noTitle: false
275 | },
276 | quote: {
277 | type: "quote",
278 | color: "158, 158, 158",
279 | icon: {
280 | type: "font-awesome",
281 | name: "quote-right"
282 | },
283 | command: false,
284 | noTitle: false
285 | },
286 | cite: {
287 | type: "quote",
288 | color: "158, 158, 158",
289 | icon: {
290 | type: "font-awesome",
291 | name: "quote-right"
292 | },
293 | command: false,
294 | noTitle: false
295 | }
296 | };
297 |
--------------------------------------------------------------------------------
/src/util/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./util";
2 | export * from "./constants";
3 |
--------------------------------------------------------------------------------
/src/util/util.ts:
--------------------------------------------------------------------------------
1 | import { Notice } from "obsidian";
2 | import { Admonition } from "../@types";
3 |
4 | function startsWithAny(str: string, needles: string[]) {
5 | for (let i = 0; i < needles.length; i++) {
6 | if (str.startsWith(needles[i])) {
7 | return i;
8 | }
9 | }
10 |
11 | return false;
12 | }
13 |
14 | export function getParametersFromSource(
15 | type: string,
16 | src: string,
17 | admonition: Admonition
18 | ) {
19 | const admonitionTitle =
20 | admonition.title ?? type[0].toUpperCase() + type.slice(1).toLowerCase();
21 | const keywordTokens = ["title:", "collapse:", "icon:", "color:", "metadata:"];
22 |
23 | const keywords = ["title", "collapse", "icon", "color", "metadata"];
24 |
25 | let lines = src.split("\n");
26 |
27 | let skipLines = 0;
28 |
29 | let params: { [k: string]: string } = {};
30 |
31 | for (let i = 0; i < lines.length; i++) {
32 | let keywordIndex = startsWithAny(lines[i], keywordTokens);
33 |
34 | if (keywordIndex === false) {
35 | break;
36 | }
37 |
38 | let foundKeyword = keywords[keywordIndex];
39 |
40 | if (params[foundKeyword] !== undefined) {
41 | break;
42 | }
43 |
44 | params[foundKeyword] = lines[i]
45 | .slice(keywordTokens[keywordIndex].length)
46 | .trim();
47 | ++skipLines;
48 | }
49 |
50 | let { title, collapse, icon, color, metadata } = params;
51 |
52 | // If color is in RGB format
53 | if (color && color.startsWith('rgb')) {
54 | color = color.slice(4, -1);
55 | }
56 |
57 | // If color is in Hex format, convert it to RGB
58 | if (color && color.startsWith('#')) {
59 | const hex = color.slice(1);
60 | const bigint = parseInt(hex, 16);
61 | const r = (bigint >> 16) & 255;
62 | const g = (bigint >> 8) & 255;
63 | const b = bigint & 255;
64 | color = `${r}, ${g}, ${b}`;
65 | }
66 |
67 | // If color is in HSL format, convert it to RGB
68 | if (color && color.startsWith('hsl')) {
69 | const [h, s, l] = color.slice(4, -1).split(',').map(str => Number(str.replace('%', '').trim()));
70 | const [r, g, b] = hslToRgb(h, s, l);
71 | color = `${r}, ${g}, ${b}`;
72 | }
73 |
74 | // If color is in HSB format, convert it to RGB
75 | if (color && (color.startsWith('hsb') || color.startsWith('hsv'))) {
76 | const [h, s, v] = color.slice(4, -1).split(',').map(str => Number(str.replace('%', '').trim()));
77 | const [r, g, b] = hsbToRgb(h, s, v);
78 | color = `${r}, ${g}, ${b}`;
79 | }
80 |
81 | let content = lines.slice(skipLines).join("\n");
82 |
83 | /**
84 | * If the admonition should collapse, but something other than open or closed was provided, set to closed.
85 | */
86 | if (
87 | collapse !== undefined &&
88 | collapse !== "none" &&
89 | collapse !== "open" &&
90 | collapse !== "closed"
91 | ) {
92 | collapse = "closed";
93 | }
94 |
95 | if (!("title" in params)) {
96 | if (!admonition.noTitle) {
97 | title = admonitionTitle;
98 | }
99 | }
100 | /**
101 | * If the admonition should collapse, but title was blanked, set the default title.
102 | */
103 | if (
104 | title &&
105 | title.trim() === "" &&
106 | collapse !== undefined &&
107 | collapse !== "none"
108 | ) {
109 | title = admonitionTitle;
110 | new Notice("An admonition must have a title if it is collapsible.");
111 | }
112 |
113 | return { title, collapse, content, icon, color, metadata };
114 | }
115 |
116 | function hslToRgb(h: number, s: number, l: number) {
117 | h /= 360;
118 | s /= 100;
119 | l /= 100;
120 | let r, g, b;
121 |
122 | if (s === 0) {
123 | r = g = b = l; // achromatic
124 | } else {
125 | const hue2rgb = (p: number, q: number, t: number) => {
126 | if (t < 0) t += 1;
127 | if (t > 1) t -= 1;
128 | if (t < 1 / 6) return p + (q - p) * 6 * t;
129 | if (t < 1 / 2) return q;
130 | if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
131 | return p;
132 | };
133 | const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
134 | const p = 2 * l - q;
135 | r = hue2rgb(p, q, h + 1 / 3);
136 | g = hue2rgb(p, q, h);
137 | b = hue2rgb(p, q, h - 1 / 3);
138 | }
139 |
140 | return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
141 | }
142 |
143 | function hsbToRgb(h: number, s: number, b: number) {
144 | h /= 360;
145 | s /= 100;
146 | b /= 100;
147 | let r, g, bb;
148 | let i = Math.floor(h * 6);
149 | let f = h * 6 - i;
150 | let p = b * (1 - s);
151 | let q = b * (1 - f * s);
152 | let t = b * (1 - (1 - f) * s);
153 | switch (i % 6) {
154 | case 0: r = b, g = t, bb = p; break;
155 | case 1: r = q, g = b, bb = p; break;
156 | case 2: r = p, g = b, bb = t; break;
157 | case 3: r = p, g = q, bb = b; break;
158 | case 4: r = t, g = p, bb = b; break;
159 | case 5: r = b, g = p, bb = q; break;
160 | }
161 | return [Math.round(r * 255), Math.round(g * 255), Math.round(bb * 255)];
162 | }
--------------------------------------------------------------------------------
/src/util/validator.ts:
--------------------------------------------------------------------------------
1 | import { Admonition, AdmonitionIconDefinition } from "src/@types";
2 | import { t } from "src/lang/helpers";
3 | import ObsidianAdmonition from "src/main";
4 |
5 | type ValidationSuccess = {
6 | success: true;
7 | messages?: string[];
8 | };
9 | type ValidationError = {
10 | success: false;
11 | failed: "type" | "icon" | "rgb" | "title" | "booleans";
12 | message: string;
13 | };
14 | type Result = ValidationSuccess | ValidationError;
15 |
16 | export const isSelectorValid = ((dummyElement) => (selector: string) => {
17 | try {
18 | dummyElement.querySelector(selector);
19 | } catch {
20 | return false;
21 | }
22 | return true;
23 | })(document.createDocumentFragment());
24 |
25 | export class AdmonitionValidator {
26 | static validateImport(
27 | plugin: ObsidianAdmonition,
28 | admonition: Admonition
29 | ): Result {
30 | const result: Result = {
31 | success: true,
32 | messages: []
33 | };
34 | const validType = AdmonitionValidator.validateType(
35 | admonition.type,
36 | plugin
37 | );
38 | if (validType.success == false) {
39 | return validType;
40 | }
41 | const iconName =
42 | typeof admonition.icon == "string"
43 | ? admonition.icon
44 | : typeof admonition.icon == "object"
45 | ? admonition.icon?.name
46 | : null;
47 | const validIcon = AdmonitionValidator.validateType(iconName, plugin);
48 | if (validIcon.success == false) {
49 | return validIcon;
50 | }
51 |
52 | const iconNode = plugin.iconManager.getIconNode(admonition.icon);
53 | if (!iconNode) {
54 | result.messages.push(
55 | "No installed icon found by the name " +
56 | iconName +
57 | ". Perhaps you need to install a new icon pack?"
58 | );
59 | }
60 | if (admonition.title && typeof admonition.title != "string") {
61 | return {
62 | success: false,
63 | failed: "title",
64 | message: "Admonition titles can only be strings."
65 | };
66 | }
67 | if (
68 | !("color" in admonition) ||
69 | !/(?:(?:2(?:[0-4]\d|5[0-5])|\d{1,2}|1\d\d)\s*,\s*){2}\s*(?:2(?:[0-4]\d|5[0-5])|\d{1,2}|1\d\d)/.test(
70 | admonition.color
71 | )
72 | ) {
73 | console.warn(
74 | "No color provided for the import of " +
75 | admonition.type +
76 | ". Adding a random color."
77 | );
78 | admonition.color = `${Math.floor(
79 | Math.random() * 255
80 | )}, ${Math.floor(Math.random() * 255)}, ${Math.floor(
81 | Math.random() * 255
82 | )}`;
83 | }
84 | const booleans: (keyof Admonition)[] = [
85 | "command",
86 | "injectColor",
87 | "noTitle",
88 | "copy"
89 | ];
90 | for (const key of booleans) {
91 | if (
92 | key in admonition &&
93 | typeof JSON.parse(JSON.stringify(admonition[key])) != "boolean"
94 | ) {
95 | return {
96 | success: false,
97 | failed: "booleans",
98 | message: `The "${key}" property must be a boolean if present.`
99 | };
100 | }
101 | }
102 | return result;
103 | }
104 | static validate(
105 | plugin: ObsidianAdmonition,
106 | type: string,
107 | icon: AdmonitionIconDefinition,
108 | oldType?: string
109 | ): Result {
110 | const validType = AdmonitionValidator.validateType(
111 | type,
112 | plugin,
113 | oldType
114 | );
115 | if (validType.success == false) {
116 | return validType;
117 | }
118 |
119 | return AdmonitionValidator.validateIcon(icon, plugin);
120 | }
121 | static validateType(
122 | type: string,
123 | plugin: ObsidianAdmonition,
124 | oldType?: string
125 | ): Result {
126 | if (!type.length) {
127 | return {
128 | success: false,
129 | message: t("Admonition type cannot be empty."),
130 | failed: "type"
131 | };
132 | }
133 |
134 | if (type.includes(" ")) {
135 | return {
136 | success: false,
137 | message: t("Admonition type cannot include spaces."),
138 | failed: "type"
139 | };
140 | }
141 | if (!isSelectorValid(type)) {
142 | return {
143 | success: false,
144 | message: t("Types must be a valid CSS selector."),
145 | failed: "type"
146 | };
147 | }
148 | if (type != oldType && type in plugin.data.userAdmonitions) {
149 | return {
150 | success: false,
151 | message: "That Admonition type already exists.",
152 | failed: "type"
153 | };
154 | }
155 | return { success: true };
156 | }
157 | static validateIcon(
158 | definition: AdmonitionIconDefinition,
159 | plugin: ObsidianAdmonition
160 | ): Result {
161 | if (definition.type === "image") {
162 | return {
163 | success: true
164 | };
165 | }
166 | if (!definition.name?.length) {
167 | return {
168 | success: false,
169 | message: t("Icon cannot be empty."),
170 | failed: "icon"
171 | };
172 | }
173 | const icon = plugin.iconManager.getIconType(definition.name);
174 | if (!icon) {
175 | return {
176 | success: false,
177 | message: t("Invalid icon name."),
178 | failed: "icon"
179 | };
180 | }
181 | return {
182 | success: true
183 | };
184 | }
185 | }
186 |
--------------------------------------------------------------------------------
/svelte.config.js:
--------------------------------------------------------------------------------
1 | const config = {
2 | onwarn: (warning, handler) => {
3 | if (warning.code.toLowerCase().startsWith("a11y-")) {
4 | return;
5 | }
6 | handler(warning);
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "baseUrl": ".",
5 | "inlineSourceMap": true,
6 | "inlineSources": true,
7 | "module": "ESNext",
8 | "target": "es6",
9 | "allowJs": true,
10 | "noImplicitAny": true,
11 | "moduleResolution": "node",
12 | "importHelpers": true,
13 | "lib": ["dom", "es5", "scripthost", "es2019"]
14 | },
15 | "include": ["**/*.ts"]
16 | }
17 |
--------------------------------------------------------------------------------
/versions.json:
--------------------------------------------------------------------------------
1 | {
2 | "0.2.3": "0.11.0",
3 | "1.0.1": "0.11.0",
4 | "2.0.1": "0.11.0",
5 | "3.1.2": "0.11.0",
6 | "3.2.2": "0.11.0",
7 | "3.3.4": "0.11.0",
8 | "4.0.1": "0.11.0",
9 | "4.1.7": "0.11.0",
10 | "4.2.1": "0.11.0",
11 | "4.3.1": "0.12.0",
12 | "4.4.2": "0.12.2",
13 | "5.0.3": "0.12.2",
14 | "6.2.10": "0.12.4",
15 | "6.4.1": "0.12.10",
16 | "7.0.4": "0.13.14",
17 | "8.0.0": "0.14.0",
18 | "9.2.0": "1.1.0"
19 | }
20 |
--------------------------------------------------------------------------------