diff --git a/.eslintrc b/.eslintrc index 5ae72289..ab5a27fb 100644 --- a/.eslintrc +++ b/.eslintrc @@ -37,6 +37,27 @@ "@typescript-eslint/no-explicit-any": ["error"], // allow prettier to use SmartTabs "no-mixed-spaces-and-tabs": "off", + "padding-line-between-statements": [ + "warn", + { + "blankLine": "always", + "prev": "*", + "next": [ + "return", + "if", + "multiline-const", + "function", + "multiline-expression", + "multiline-let", + "block-like" + ] + }, + { + "blankLine": "always", + "prev": ["function"], + "next": "*" + } + ], "svelte/no-at-html-tags": "off" } } diff --git a/main.ts b/main.ts index dd99a339..89c3140f 100644 --- a/main.ts +++ b/main.ts @@ -74,6 +74,7 @@ export default class DigitalGarden extends Plugin { await this.addCommands(); addIcon("digital-garden-icon", seedling); + this.addRibbonIcon( "digital-garden-icon", "Digital Garden Publication Center", @@ -105,12 +106,14 @@ export default class DigitalGarden extends Plugin { new Notice("Adding publish flag to note and publishing it."); await this.setPublishFlagValue(true); const activeFile = this.app.workspace.getActiveFile(); + const event = this.app.metadataCache.on( "changed", async (file, _data, _cache) => { if (file.path === activeFile?.path) { const successfullyPublished = await this.publishSingleNote(); + if (successfullyPublished) { await this.copyGardenUrlToClipboard(); } @@ -139,18 +142,22 @@ export default class DigitalGarden extends Plugin { name: "Publish Multiple Notes", callback: async () => { const statusBarItem = this.addStatusBarItem(); + try { new Notice("Processing files to publish..."); const { vault, metadataCache } = this.app; + const publisher = new Publisher( vault, metadataCache, this.settings, ); + const siteManager = new DigitalGardenSiteManager( metadataCache, this.settings, ); + const publishStatusManager = new PublishStatusManager( siteManager, publisher, @@ -158,11 +165,13 @@ export default class DigitalGarden extends Plugin { const publishStatus = await publishStatusManager.getPublishStatus(); + const filesToPublish = publishStatus.changedNotes.concat( publishStatus.unpublishedNotes, ); const filesToDelete = publishStatus.deletedNotePaths; const imagesToDelete = publishStatus.deletedImagePaths; + const statusBar = new PublishStatusBar( statusBarItem, filesToPublish.length + @@ -173,27 +182,32 @@ export default class DigitalGarden extends Plugin { let errorFiles = 0; let errorDeleteFiles = 0; let errorDeleteImage = 0; + new Notice( `Publishing ${filesToPublish.length} notes, deleting ${filesToDelete.length} notes and ${imagesToDelete.length} images. See the status bar in lower right corner for progress.`, 8000, ); + for (const file of filesToPublish) { try { statusBar.increment(); await publisher.publish(file); } catch { errorFiles++; + new Notice( `Unable to publish note ${file.name}, skipping it.`, ); } } + for (const filePath of filesToDelete) { try { statusBar.increment(); await publisher.deleteNote(filePath); } catch { errorDeleteFiles++; + new Notice( `Unable to delete note ${filePath}, skipping it.`, ); @@ -206,6 +220,7 @@ export default class DigitalGarden extends Plugin { await publisher.deleteImage(filePath); } catch { errorDeleteImage++; + new Notice( `Unable to delete image ${filePath}, skipping it.`, ); @@ -213,11 +228,13 @@ export default class DigitalGarden extends Plugin { } statusBar.finish(8000); + new Notice( `Successfully published ${ filesToPublish.length - errorFiles } notes to your garden.`, ); + if (filesToDelete.length > 0) { new Notice( `Successfully deleted ${ @@ -225,6 +242,7 @@ export default class DigitalGarden extends Plugin { } notes from your garden.`, ); } + if (imagesToDelete.length > 0) { new Notice( `Successfully deleted ${ @@ -235,6 +253,7 @@ export default class DigitalGarden extends Plugin { } catch (e) { statusBarItem.remove(); console.error(e); + new Notice( "Unable to publish multiple notes, something went wrong.", ); @@ -297,6 +316,7 @@ export default class DigitalGarden extends Plugin { async copyGardenUrlToClipboard() { try { const { metadataCache, workspace } = this.app; + const activeFile = this.getActiveFile(workspace); if (!activeFile) { @@ -313,6 +333,7 @@ export default class DigitalGarden extends Plugin { new Notice(`Note URL copied to clipboard`); } catch (e) { console.log(e); + new Notice( "Unable to copy note URL to clipboard, something went wrong.", ); @@ -323,6 +344,7 @@ export default class DigitalGarden extends Plugin { try { const { vault, workspace, metadataCache } = this.app; + const activeFile = this.getActiveFile(workspace); if (!activeFile) { @@ -332,10 +354,12 @@ export default class DigitalGarden extends Plugin { new Notice( "The current file is not a markdown file. Please open a markdown file and try again.", ); + return; } new Notice("Publishing note..."); + const publisher = new Publisher( vault, metadataCache, @@ -346,13 +370,16 @@ export default class DigitalGarden extends Plugin { if (publishSuccessful) { new Notice(`Successfully published note to your garden.`); } + return publishSuccessful; } catch (e) { console.error(e); new Notice("Unable to publish note, something went wrong."); + return false; } } + async setPublishFlagValue(value: boolean) { const activeFile = this.getActiveFile(this.app.workspace); @@ -392,15 +419,18 @@ export default class DigitalGarden extends Plugin { this.app.metadataCache, this.settings, ); + const publisher = new Publisher( this.app.vault, this.app.metadataCache, this.settings, ); + const publishStatusManager = new PublishStatusManager( siteManager, publisher, ); + this.publishModal = new PublishModal( this.app, publishStatusManager, diff --git a/src/publisher/DigitalGardenSiteManager.ts b/src/publisher/DigitalGardenSiteManager.ts index b9528c0a..5b5b8b7b 100644 --- a/src/publisher/DigitalGardenSiteManager.ts +++ b/src/publisher/DigitalGardenSiteManager.ts @@ -51,6 +51,7 @@ export default class DigitalGardenSiteManager { } let envSettings = ""; + if (theme.name !== "default") { envSettings = `THEME=${theme.cssUrl}\nBASE_THEME=${baseTheme}`; } @@ -76,6 +77,7 @@ export default class DigitalGardenSiteManager { let fileExists = true; let currentFile = null; + try { currentFile = await octokit.request( "GET /repos/{owner}/{repo}/contents/{path}", @@ -108,6 +110,7 @@ export default class DigitalGardenSiteManager { // caught in copyUrlToClipboard throw new Error("Garden base url not set"); } + const baseUrl = `https://${extractBaseUrl( this.settings.gardenBaseUrl, )}`; @@ -137,6 +140,7 @@ export default class DigitalGardenSiteManager { path = path.substring(1); } const octokit = new Octokit({ auth: this.settings.githubToken }); + const response = await octokit.request( `GET /repos/{owner}/{repo}/contents/{path}`, { @@ -148,11 +152,13 @@ export default class DigitalGardenSiteManager { // @ts-expect-error data is not yet type-guarded const content = Base64.decode(response.data.content); + return content; } async getNoteHashes(): Promise> { const octokit = new Octokit({ auth: this.settings.githubToken }); + // Force the cache to be updated const response = await octokit.request( `GET /repos/{owner}/{repo}/git/trees/{tree_sha}?recursive=${Math.ceil( @@ -166,6 +172,7 @@ export default class DigitalGardenSiteManager { ); const files = response.data.tree; + const notes: Array<{ path: string; sha: string }> = files.filter( (x: { path: string; type: string }) => x.path.startsWith("src/site/notes/") && @@ -173,15 +180,18 @@ export default class DigitalGardenSiteManager { x.path !== "src/site/notes/notes.json", ); const hashes: Record = {}; + for (const note of notes) { const vaultPath = note.path.replace("src/site/notes/", ""); hashes[vaultPath] = note.sha; } + return hashes; } async getImageHashes(): Promise> { const octokit = new Octokit({ auth: this.settings.githubToken }); + // Force the cache to be updated const response = await octokit.request( `GET /repos/{owner}/{repo}/git/trees/{tree_sha}?recursive=${Math.ceil( @@ -195,17 +205,20 @@ export default class DigitalGardenSiteManager { ); const files = response.data.tree; + const images: Array<{ path: string; sha: string }> = files.filter( (x: { path: string; type: string }) => x.path.startsWith("src/site/img/user/") && x.type === "blob", ); const hashes: Record = {}; + for (const img of images) { const vaultPath = decodeURI( img.path.replace("src/site/img/user/", ""), ); hashes[vaultPath] = img.sha; } + return hashes; } @@ -215,6 +228,7 @@ export default class DigitalGardenSiteManager { */ async createPullRequestWithSiteChanges(): Promise { const octokit = new Octokit({ auth: this.settings.githubToken }); + const latestRelease = await octokit.request( "GET /repos/{owner}/{repo}/releases/latest", { @@ -225,6 +239,7 @@ export default class DigitalGardenSiteManager { const templateVersion = latestRelease.data.tag_name; const uuid = crypto.randomUUID(); + const branchName = "update-template-to-v" + templateVersion + "-" + uuid; @@ -246,6 +261,7 @@ export default class DigitalGardenSiteManager { branchName, templateVersion, ); + return prUrl; } @@ -299,6 +315,7 @@ export default class DigitalGardenSiteManager { ref: branchName, }, ); + await octokit.request( "DELETE /repos/{owner}/{repo}/contents/{path}", { @@ -334,6 +351,7 @@ export default class DigitalGardenSiteManager { let currentFile = {}; let fileExists = true; + try { currentFile = await octokit.request( "GET /repos/{owner}/{repo}/contents/{path}", @@ -351,6 +369,7 @@ export default class DigitalGardenSiteManager { const fileHasChanged = // @ts-expect-error data is not yet type-guarded latestFile.data.sha !== currentFile?.data?.sha; + if (!fileExists || fileHasChanged) { // commit await octokit.request( @@ -448,6 +467,7 @@ export default class DigitalGardenSiteManager { // @ts-expect-error data is not yet type-guarded Base64.decode(pluginInfoResponse.data.content), ); + return pluginInfo as DigitalGardenPluginInfo; } } diff --git a/src/publisher/ObsidianFrontMatterEngine.ts b/src/publisher/ObsidianFrontMatterEngine.ts index 88c0e2f3..85b1cdcb 100644 --- a/src/publisher/ObsidianFrontMatterEngine.ts +++ b/src/publisher/ObsidianFrontMatterEngine.ts @@ -25,11 +25,13 @@ export default class ObsidianFrontMatterEngine implements IFrontMatterEngine { value: string | boolean | number, ): ObsidianFrontMatterEngine { this.generatedFrontMatter[key] = value; + return this; } remove(key: string): ObsidianFrontMatterEngine { this.generatedFrontMatter[key] = undefined; + return this; } @@ -44,6 +46,7 @@ export default class ObsidianFrontMatterEngine implements IFrontMatterEngine { const frontmatterRegex = /^\s*?---\n([\s\S]*?)\n---/g; const yaml = this.frontMatterToYaml(newFrontMatter); let newContent = ""; + if (content.match(frontmatterRegex)) { newContent = content.replace(frontmatterRegex, (_match) => { return yaml; @@ -67,10 +70,12 @@ export default class ObsidianFrontMatterEngine implements IFrontMatterEngine { } let yaml = "---\n"; + for (const key of Object.keys(frontMatter)) { yaml += `${key}: ${frontMatter[key]}\n`; } yaml += "---"; + return yaml; } diff --git a/src/publisher/PublishStatusManager.ts b/src/publisher/PublishStatusManager.ts index 31157c48..63a50b34 100644 --- a/src/publisher/PublishStatusManager.ts +++ b/src/publisher/PublishStatusManager.ts @@ -17,6 +17,7 @@ export default class PublishStatusManager implements IPublishStatusManager { async getDeletedNotePaths(): Promise> { const remoteNoteHashes = await this.siteManager.getNoteHashes(); const marked = await this.publisher.getFilesMarkedForPublishing(); + return this.generateDeletedContentPaths( remoteNoteHashes, marked.notes.map((f) => f.path), @@ -26,6 +27,7 @@ export default class PublishStatusManager implements IPublishStatusManager { async getDeletedImagesPaths(): Promise> { const remoteImageHashes = await this.siteManager.getImageHashes(); const marked = await this.publisher.getFilesMarkedForPublishing(); + return this.generateDeletedContentPaths( remoteImageHashes, marked.images, @@ -37,6 +39,7 @@ export default class PublishStatusManager implements IPublishStatusManager { marked: string[], ): Array { const deletedContentPaths: Array = []; + Object.keys(remoteNoteHashes).forEach((key) => { if (!key.endsWith(".js") && !marked.find((f) => f === key)) { deletedContentPaths.push(key); @@ -53,11 +56,13 @@ export default class PublishStatusManager implements IPublishStatusManager { const remoteNoteHashes = await this.siteManager.getNoteHashes(); const remoteImageHashes = await this.siteManager.getImageHashes(); const marked = await this.publisher.getFilesMarkedForPublishing(); + for (const file of marked.notes) { const [content, _] = await this.publisher.generateMarkdown(file); const localHash = generateBlobHash(content); const remoteHash = remoteNoteHashes[file.path]; + if (!remoteHash) { unpublishedNotes.push(file); } else if (remoteHash === localHash) { @@ -71,6 +76,7 @@ export default class PublishStatusManager implements IPublishStatusManager { remoteNoteHashes, marked.notes.map((f) => f.path), ); + const deletedImagePaths = this.generateDeletedContentPaths( remoteImageHashes, marked.images, @@ -79,6 +85,7 @@ export default class PublishStatusManager implements IPublishStatusManager { publishedNotes.sort((a, b) => (a.path > b.path ? 1 : -1)); changedNotes.sort((a, b) => (a.path > b.path ? 1 : -1)); deletedNotePaths.sort((a, b) => (a > b ? 1 : -1)); + return { unpublishedNotes, publishedNotes, diff --git a/src/publisher/Publisher.ts b/src/publisher/Publisher.ts index 87136681..02d02a2c 100644 --- a/src/publisher/Publisher.ts +++ b/src/publisher/Publisher.ts @@ -98,12 +98,15 @@ export default class Publisher { const files = this.vault.getMarkdownFiles(); const notesToPublish: TFile[] = []; const imagesToPublish: Set = new Set(); + for (const file of files) { try { const frontMatter = this.metadataCache.getCache(file.path) ?.frontmatter; + if (frontMatter && frontMatter["dg-publish"] === true) { notesToPublish.push(file); + const images = await this.extractImageLinks( await this.vault.cachedRead(file), file.path, @@ -123,11 +126,13 @@ export default class Publisher { async deleteNote(vaultFilePath: string) { const path = `src/site/notes/${vaultFilePath}`; + return await this.delete(path); } async deleteImage(vaultFilePath: string) { const path = `src/site/img/user/${encodeURI(vaultFilePath)}`; + return await this.delete(path); } @@ -138,12 +143,14 @@ export default class Publisher { ); throw {}; } + if (!this.settings.githubUserName) { new Notice( "Config error: You need to define a GitHub Username in the plugin settings", ); throw {}; } + if (!this.settings.githubToken) { new Notice( "Config error: You need to define a GitHub Token in the plugin settings", @@ -170,6 +177,7 @@ export default class Publisher { path, }, ); + // @ts-expect-error TODO: abstract octokit response if (response.status === 200 && response?.data.type === "file") { // @ts-expect-error TODO: abstract octokit response @@ -177,6 +185,7 @@ export default class Publisher { } } catch (e) { console.log(e); + return false; } @@ -187,8 +196,10 @@ export default class Publisher { ); } catch (e) { console.log(e); + return false; } + return true; } @@ -200,10 +211,12 @@ export default class Publisher { ) { return false; } + try { const [text, assets] = await this.generateMarkdown(file); await this.uploadText(file.path, text); await this.uploadAssets(assets); + return true; } catch { return false; @@ -214,6 +227,7 @@ export default class Publisher { this.rewriteRules = getRewriteRules(this.settings.pathRewriteRules); const assets: Assets = { images: [] }; + if (file.name.endsWith(".excalidraw.md")) { return [await this.generateExcalidrawMarkdown(file, true), assets]; } @@ -228,6 +242,7 @@ export default class Publisher { text = await this.removeObsidianComments(text); text = await this.createSvgEmbeds(text, file.path); const text_and_images = await this.convertImageLinks(text, file.path); + return [text_and_images[0], { images: text_and_images[1] }]; } @@ -238,21 +253,25 @@ export default class Publisher { filter.replace, ); } + return text; } async createBlockIDs(text: string) { const block_pattern = / \^([\w\d-]+)/g; const complex_block_pattern = /\n\^([\w\d-]+)\n/g; + text = text.replace( complex_block_pattern, (match: string, $1: string) => { return `{ #${$1}}\n\n`; }, ); + text = text.replace(block_pattern, (match: string, $1: string) => { return `\n{ #${$1}}\n`; }); + return text; } @@ -263,12 +282,14 @@ export default class Publisher { ); throw {}; } + if (!this.settings.githubUserName) { new Notice( "Config error: You need to define a GitHub Username in the plugin settings", ); throw {}; } + if (!this.settings.githubToken) { new Notice( "Config error: You need to define a GitHub Token in the plugin settings", @@ -296,6 +317,7 @@ export default class Publisher { path, }, ); + // @ts-expect-error TODO: abstract octokit response if (response.status === 200 && response.data.type === "file") { // @ts-expect-error TODO: abstract octokit response @@ -336,6 +358,7 @@ export default class Publisher { textToBeProcessed = textToBeProcessed.replace(this.excaliDrawRegex, ""); textToBeProcessed = textToBeProcessed.replace(this.codeBlockRegex, ""); textToBeProcessed = textToBeProcessed.replace(this.codeFenceRegex, ""); + textToBeProcessed = textToBeProcessed.replace( this.frontmatterRegex, "", @@ -359,6 +382,7 @@ export default class Publisher { ) { continue; } + if ( codeFences.findIndex((x) => x.contains(commentMatch)) > -1 ) { @@ -379,9 +403,11 @@ export default class Publisher { async convertFrontMatter(text: string, file: TFile): Promise { const publishedFrontMatter = this.getProcessedFrontMatter(file); + const replaced = text.replace(this.frontmatterRegex, (_match, _p1) => { return publishedFrontMatter; }); + return replaced; } @@ -389,10 +415,12 @@ export default class Publisher { let replacedText = text; const dataViewRegex = /```dataview\s(.+?)```/gms; const dvApi = getAPI(); + if (!dvApi) return replacedText; const matches = text.matchAll(dataViewRegex); const dataviewJsPrefix = dvApi.settings.dataviewJsKeyword; + const dataViewJsRegex = new RegExp( "```" + escapeRegExp(dataviewJsPrefix) + "\\s(.+?)```", "gsm", @@ -400,6 +428,7 @@ export default class Publisher { const dataviewJsMatches = text.matchAll(dataViewJsRegex); const inlineQueryPrefix = dvApi.settings.inlineQueryPrefix; + const inlineDataViewRegex = new RegExp( "`" + escapeRegExp(inlineQueryPrefix) + "(.+?)`", "gsm", @@ -407,6 +436,7 @@ export default class Publisher { const inlineMatches = text.matchAll(inlineDataViewRegex); const inlineJsQueryPrefix = dvApi.settings.inlineJsQueryPrefix; + const inlineJsDataViewRegex = new RegExp( "`" + escapeRegExp(inlineJsQueryPrefix) + "(.+?)`", "gsm", @@ -428,15 +458,18 @@ export default class Publisher { const block = queryBlock[0]; const query = queryBlock[1]; const markdown = await dvApi.tryQueryMarkdown(query, path); + replacedText = replacedText.replace( block, `${markdown}\n{ .block-language-dataview}`, ); } catch (e) { console.log(e); + new Notice( "Unable to render dataview query. Please update the dataview plugin to the latest version.", ); + return queryBlock[0]; } } @@ -454,9 +487,11 @@ export default class Publisher { replacedText = replacedText.replace(block, div.innerHTML); } catch (e) { console.log(e); + new Notice( "Unable to render dataviewjs query. Please update the dataview plugin to the latest version.", ); + return queryBlock[0]; } } @@ -466,10 +501,12 @@ export default class Publisher { try { const code = inlineQuery[0]; const query = inlineQuery[1]; + const dataviewResult = dvApi.tryEvaluate(query, { // @ts-expect-error errors are caught this: dvApi.page(path), }); + if (dataviewResult) { replacedText = replacedText.replace( code, @@ -479,9 +516,11 @@ export default class Publisher { } } catch (e) { console.log(e); + new Notice( "Unable to render inline dataview query. Please update the dataview plugin to the latest version.", ); + return inlineQuery[0]; } } @@ -499,9 +538,11 @@ export default class Publisher { replacedText = replacedText.replace(code, div.innerHTML); } catch (e) { console.log(e); + new Notice( "Unable to render inline dataviewjs query. Please update the dataview plugin to the latest version.", ); + return inlineJsQuery[0]; } } @@ -524,26 +565,32 @@ export default class Publisher { publishedFrontMatter, file.path, ); + publishedFrontMatter = this.addDefaultPassThrough( fileFrontMatter, publishedFrontMatter, ); + publishedFrontMatter = this.addContentClasses( fileFrontMatter, publishedFrontMatter, ); + publishedFrontMatter = this.addPageTags( fileFrontMatter, publishedFrontMatter, ); + publishedFrontMatter = this.addFrontMatterSettings( fileFrontMatter, publishedFrontMatter, ); + publishedFrontMatter = this.addNoteIconFrontMatter( fileFrontMatter, publishedFrontMatter, ); + publishedFrontMatter = this.addTimestampsFrontmatter( fileFrontMatter, publishedFrontMatter, @@ -599,6 +646,7 @@ export default class Publisher { filePath: string, ) { const publishedFrontMatter = { ...newFrontMatter }; + const gardenPath = baseFrontMatter && baseFrontMatter["dg-path"] ? baseFrontMatter["dg-path"] @@ -611,6 +659,7 @@ export default class Publisher { if (baseFrontMatter && baseFrontMatter["dg-permalink"]) { publishedFrontMatter["dg-permalink"] = baseFrontMatter["dg-permalink"]; + publishedFrontMatter["permalink"] = sanitizePermalink( baseFrontMatter["dg-permalink"], ); @@ -642,6 +691,7 @@ export default class Publisher { publishedFrontMatter["tags"] = tags; } } + return publishedFrontMatter; } @@ -690,6 +740,7 @@ export default class Publisher { const publishedFrontMatter = { ...newFrontMatter }; const createdKey = this.settings.createdTimestampKey; const updatedKey = this.settings.updatedTimestampKey; + if (createdKey.length) { if (typeof baseFrontMatter[createdKey] == "string") { publishedFrontMatter["created"] = baseFrontMatter[createdKey]; @@ -701,6 +752,7 @@ export default class Publisher { file.stat.ctime, ).toISO(); } + if (updatedKey.length) { if (typeof baseFrontMatter[updatedKey] == "string") { publishedFrontMatter["updated"] = baseFrontMatter[updatedKey]; @@ -712,6 +764,7 @@ export default class Publisher { file.stat.mtime, ).toISO(); } + return publishedFrontMatter; } @@ -735,11 +788,13 @@ export default class Publisher { const publishedFrontMatter = { ...newFrontMatter }; const noteIconKey = this.settings.noteIconKey; + if (baseFrontMatter[noteIconKey] !== undefined) { publishedFrontMatter["noteIcon"] = baseFrontMatter[noteIconKey]; } else { publishedFrontMatter["noteIcon"] = this.settings.defaultNoteIcon; } + return publishedFrontMatter; } @@ -751,6 +806,7 @@ export default class Publisher { baseFrontMatter = {}; } const publishedFrontMatter = { ...newFrontMatter }; + for (const key of Object.keys(this.settings.defaultNoteSettings)) { const settingValue = baseFrontMatter[kebabize(key)]; @@ -758,6 +814,7 @@ export default class Publisher { publishedFrontMatter[key] = settingValue; } } + const dgPassFrontmatter = this.settings.defaultNoteSettings.dgPassFrontmatter; @@ -786,8 +843,10 @@ export default class Publisher { linkMatch.indexOf("[") + 2, linkMatch.lastIndexOf("]") - 1, ); + let [linkedFileName, prettyName] = textInsideBrackets.split("|"); + if (linkedFileName.endsWith("\\")) { linkedFileName = linkedFileName.substring( 0, @@ -797,29 +856,35 @@ export default class Publisher { prettyName = prettyName || linkedFileName; let headerPath = ""; + if (linkedFileName.includes("#")) { const headerSplit = linkedFileName.split("#"); linkedFileName = headerSplit[0]; + //currently no support for linking to nested heading with multiple #s headerPath = headerSplit.length > 1 ? `#${headerSplit[1]}` : ""; } const fullLinkedFilePath = getLinkpath(linkedFileName); + const linkedFile = this.metadataCache.getFirstLinkpathDest( fullLinkedFilePath, filePath, ); + if (!linkedFile) { convertedText = convertedText.replace( linkMatch, `[[${linkedFileName}${headerPath}\\|${prettyName}]]`, ); } + if (linkedFile?.extension === "md") { const extensionlessPath = linkedFile.path.substring( 0, linkedFile.path.lastIndexOf("."), ); + convertedText = convertedText.replace( linkMatch, `[[${extensionlessPath}${headerPath}\\|${prettyName}]]`, @@ -843,34 +908,42 @@ export default class Publisher { if (currentDepth >= 4) { return text; } + const { notes: publishedFiles } = await this.getFilesMarkedForPublishing(); let transcludedText = text; const transcludedRegex = /!\[\[(.+?)\]\]/g; const transclusionMatches = text.match(transcludedRegex); let numberOfExcaliDraws = 0; + if (transclusionMatches) { for (let i = 0; i < transclusionMatches.length; i++) { try { const transclusionMatch = transclusionMatches[i]; + const [transclusionFileName, headerName] = transclusionMatch .substring( transclusionMatch.indexOf("[") + 2, transclusionMatch.indexOf("]"), ) .split("|"); + const transclusionFilePath = getLinkpath(transclusionFileName); + const linkedFile = this.metadataCache.getFirstLinkpathDest( transclusionFilePath, filePath, ); + if (!linkedFile) { continue; } let sectionID = ""; + if (linkedFile.name.endsWith(".excalidraw.md")) { const firstDrawing = ++numberOfExcaliDraws === 1; + const excaliDrawCode = await this.generateExcalidrawMarkdown( linkedFile, @@ -885,6 +958,7 @@ export default class Publisher { ); } else if (linkedFile.extension === "md") { let fileText = await this.vault.cachedRead(linkedFile); + const metadata = this.metadataCache.getFileCache(linkedFile); @@ -893,8 +967,10 @@ export default class Publisher { const refBlock = transclusionFileName.split("#^")[1]; sectionID = `#${slugify(refBlock)}`; + const blockInFile = metadata?.blocks && metadata.blocks[refBlock]; + if (blockInFile) { fileText = fileText .split("\n") @@ -915,9 +991,11 @@ export default class Publisher { ); sectionID = `#${slugify(refHeader)}`; + if (headerInFile && metadata?.headings) { const headerPosition = metadata.headings.indexOf(headerInFile); + // Embed should copy the content proparly under the given block const cutTo = metadata.headings .slice(headerPosition + 1) @@ -925,9 +1003,11 @@ export default class Publisher { (header) => header.level <= headerInFile.level, ); + if (cutTo) { const cutToLine = cutTo?.position?.start?.line; + fileText = fileText .split("\n") .slice( @@ -961,14 +1041,17 @@ export default class Publisher { ? `$
\n\n${header}\n\n
\n` : ""; let embedded_link = ""; + const publishedFilesContainsLinkedFile = publishedFiles.find( (f) => f.path == linkedFile.path, ); + if (publishedFilesContainsLinkedFile) { const permalink = metadata?.frontmatter && metadata.frontmatter["dg-permalink"]; + const gardenPath = permalink ? sanitizePermalink(permalink) : `/${generateUrlPath( @@ -979,6 +1062,7 @@ export default class Publisher { )}`; embedded_link = ``; } + fileText = `\n
${embedded_link}
\n\n${headerSection}\n\n` + fileText + @@ -991,6 +1075,7 @@ export default class Publisher { currentDepth + 1, ); } + //This should be recursive up to a certain depth transcludedText = transcludedText.replace( transclusionMatch, @@ -1014,12 +1099,15 @@ export default class Publisher { svgElement.setAttribute("width", size); fixSvgForXmlSerializer(svgElement); const svgSerializer = new XMLSerializer(); + return svgSerializer.serializeToString(svgDoc); } + //![[image.svg]] const transcludedSvgRegex = /!\[\[(.*?)(\.(svg))\|(.*?)\]\]|!\[\[(.*?)(\.(svg))\]\]/g; const transcludedSvgs = text.match(transcludedSvgRegex); + if (transcludedSvgs) { for (const svg of transcludedSvgs) { try { @@ -1027,6 +1115,7 @@ export default class Publisher { .substring(svg.indexOf("[") + 2, svg.indexOf("]")) .split("|"); const imagePath = getLinkpath(imageName); + const linkedFile = this.metadataCache.getFirstLinkpathDest( imagePath, filePath, @@ -1037,6 +1126,7 @@ export default class Publisher { } let svgText = await this.vault.read(linkedFile); + if (svgText && size) { svgText = setWidth(svgText, size); } @@ -1050,6 +1140,7 @@ export default class Publisher { //!()[image.svg] const linkedSvgRegex = /!\[(.*?)\]\((.*?)(\.(svg))\)/g; const linkedSvgMatches = text.match(linkedSvgRegex); + if (linkedSvgMatches) { for (const svg of linkedSvgMatches) { try { @@ -1059,6 +1150,7 @@ export default class Publisher { const pathStart = svg.lastIndexOf("(") + 1; const pathEnd = svg.lastIndexOf(")"); const imagePath = svg.substring(pathStart, pathEnd); + if (imagePath.startsWith("http")) { continue; } @@ -1067,11 +1159,13 @@ export default class Publisher { imagePath, filePath, ); + if (!linkedFile) { continue; } let svgText = await this.vault.read(linkedFile); + if (svgText && size) { svgText = setWidth(svgText, size); } @@ -1092,6 +1186,7 @@ export default class Publisher { const transcludedImageRegex = /!\[\[(.*?)(\.(png|jpg|jpeg|gif))\|(.*?)\]\]|!\[\[(.*?)(\.(png|jpg|jpeg|gif))\]\]/g; const transcludedImageMatches = text.match(transcludedImageRegex); + if (transcludedImageMatches) { for (let i = 0; i < transcludedImageMatches.length; i++) { try { @@ -1104,6 +1199,7 @@ export default class Publisher { ) .split("|"); const imagePath = getLinkpath(imageName); + const linkedFile = this.metadataCache.getFirstLinkpathDest( imagePath, filePath, @@ -1123,6 +1219,7 @@ export default class Publisher { //![](image.png) const imageRegex = /!\[(.*?)\]\((.*?)(\.(png|jpg|jpeg|gif))\)/g; const imageMatches = text.match(imageRegex); + if (imageMatches) { for (let i = 0; i < imageMatches.length; i++) { try { @@ -1131,15 +1228,18 @@ export default class Publisher { const pathStart = imageMatch.lastIndexOf("(") + 1; const pathEnd = imageMatch.lastIndexOf(")"); const imagePath = imageMatch.substring(pathStart, pathEnd); + if (imagePath.startsWith("http")) { continue; } const decodedImagePath = decodeURI(imagePath); + const linkedFile = this.metadataCache.getFirstLinkpathDest( decodedImagePath, filePath, ); + if (!linkedFile) { continue; } @@ -1150,6 +1250,7 @@ export default class Publisher { } } } + return assets; } @@ -1160,10 +1261,12 @@ export default class Publisher { const assets = []; let imageText = text; + //![[image.png]] const transcludedImageRegex = /!\[\[(.*?)(\.(png|jpg|jpeg|gif))\|(.*?)\]\]|!\[\[(.*?)(\.(png|jpg|jpeg|gif))\]\]/g; const transcludedImageMatches = text.match(transcludedImageRegex); + if (transcludedImageMatches) { for (let i = 0; i < transcludedImageMatches.length; i++) { try { @@ -1176,10 +1279,12 @@ export default class Publisher { ) .split("|"); const imagePath = getLinkpath(imageName); + const linkedFile = this.metadataCache.getFirstLinkpathDest( imagePath, filePath, ); + if (!linkedFile) { continue; } @@ -1188,6 +1293,7 @@ export default class Publisher { const cmsImgPath = `/img/user/${linkedFile.path}`; const name = size ? `${imageName}|${size}` : imageName; + const imageMarkdown = `![${name}](${encodeURI( cmsImgPath, )})`; @@ -1204,6 +1310,7 @@ export default class Publisher { //![](image.png) const imageRegex = /!\[(.*?)\]\((.*?)(\.(png|jpg|jpeg|gif))\)/g; const imageMatches = text.match(imageRegex); + if (imageMatches) { for (let i = 0; i < imageMatches.length; i++) { try { @@ -1216,15 +1323,18 @@ export default class Publisher { const pathStart = imageMatch.lastIndexOf("(") + 1; const pathEnd = imageMatch.lastIndexOf(")"); const imagePath = imageMatch.substring(pathStart, pathEnd); + if (imagePath.startsWith("http")) { continue; } const decodedImagePath = decodeURI(imagePath); + const linkedFile = this.metadataCache.getFirstLinkpathDest( decodedImagePath, filePath, ); + if (!linkedFile) { continue; } @@ -1252,6 +1362,7 @@ export default class Publisher { } const titleVariable = "{{title}}"; + if (headerName.includes(titleVariable)) { headerName = headerName.replace( titleVariable, @@ -1274,10 +1385,12 @@ export default class Publisher { const frontMatter = await this.getProcessedFrontMatter(file); const isCompressed = fileText.includes("```compressed-json"); + const start = fileText.indexOf(isCompressed ? "```compressed-json" : "```json") + (isCompressed ? "```compressed-json" : "```json").length; const end = fileText.lastIndexOf("```"); + const excaliDrawJson = JSON.parse( isCompressed ? LZString.decompressFromBase64( @@ -1289,6 +1402,7 @@ export default class Publisher { const drawingId = file.name.split(" ").join("_").replace(".", "") + idAppendage; let excaliDrawCode = ""; + if (includeExcaliDrawJs) { excaliDrawCode += excaliDrawBundle; } diff --git a/src/publisher/Validator.ts b/src/publisher/Validator.ts index fcbb3f89..2ca86291 100644 --- a/src/publisher/Validator.ts +++ b/src/publisher/Validator.ts @@ -7,7 +7,9 @@ export function isPublishFrontmatterValid( new Notice( "Note does not have the dg-publish: true set. Please add this and try again.", ); + return false; } + return true; } diff --git a/src/test/Publisher.test.ts b/src/test/Publisher.test.ts index 9a9325d2..ded34372 100644 --- a/src/test/Publisher.test.ts +++ b/src/test/Publisher.test.ts @@ -17,6 +17,7 @@ describe("Publisher", () => { it("should replace {{title}} with the basename of the file", () => { const testPublisher = getTestPublisher({}); const EXPECTED_TITLE = "expected"; + const result = testPublisher.generateTransclusionHeader( "# {{title}}", { basename: EXPECTED_TITLE } as TFile, @@ -24,8 +25,10 @@ describe("Publisher", () => { expect(result).toBe(`# ${EXPECTED_TITLE}`); }); + it("should add # to header if it is not a markdown header", () => { const testPublisher = getTestPublisher({}); + const result = testPublisher.generateTransclusionHeader( "header", {} as TFile, @@ -33,8 +36,10 @@ describe("Publisher", () => { expect(result).toBe(`# header`); }); + it("Ensures that header has space after #", () => { const testPublisher = getTestPublisher({}); + const result = testPublisher.generateTransclusionHeader( "###header", {} as TFile, @@ -42,8 +47,10 @@ describe("Publisher", () => { expect(result).toBe(`### header`); }); + it("Returns undefined if heading is undefined", () => { const testPublisher = getTestPublisher({}); + const result = testPublisher.generateTransclusionHeader( undefined, {} as TFile, diff --git a/src/ui/DigitalGardenSettingTab.ts b/src/ui/DigitalGardenSettingTab.ts index ce091ad3..a62ee358 100644 --- a/src/ui/DigitalGardenSettingTab.ts +++ b/src/ui/DigitalGardenSettingTab.ts @@ -24,6 +24,7 @@ export class DigitalGardenSettingTab extends PluginSettingTab { async display(): Promise { const { containerEl } = this; + const settingView = new SettingView( this.app, containerEl, @@ -57,6 +58,7 @@ export class DigitalGardenSettingTab extends PluginSettingTab { } }; settingView.renderCreatePr(prModal, handlePR); + settingView.renderPullRequestHistory( prModal, this.plugin.settings.prHistory.reverse().slice(0, 10), diff --git a/src/ui/PublishModal.ts b/src/ui/PublishModal.ts index 02297b32..ed5f39e4 100644 --- a/src/ui/PublishModal.ts +++ b/src/ui/PublishModal.ts @@ -49,9 +49,11 @@ export class PublishModal { getIcon(name: string): Node { const icon = getIcon(name) ?? document.createElement("span"); + if (icon instanceof SVGSVGElement) { icon.style.marginRight = "4px"; } + return icon; } @@ -61,12 +63,14 @@ export class PublishModal { await this.siteManager.getNoteContent(notePath); const localFile = this.vault.getAbstractFileByPath(notePath); console.log(localFile); + if (localFile instanceof TFile) { const [localContent, _] = await this.publisher.generateMarkdown(localFile); const diff = Diff.diffLines(remoteContent, localContent); let diffView: DiffView | undefined; const diffModal = new Modal(this.modal.app); + diffModal.titleEl .createEl("span", { text: `${localFile.basename}` }) .prepend(this.getIcon("file-diff")); @@ -77,6 +81,7 @@ export class PublishModal { props: { diff: diff }, }); }; + this.modal.onClose = () => { if (diffView) { diffView.$destroy(); @@ -96,6 +101,7 @@ export class PublishModal { this.modal.onOpen = () => { this.modal.contentEl.empty(); + this.publicationCenterUi = new PublicationCenter({ target: this.modal.contentEl, props: { diff --git a/src/ui/PublishStatusBar.ts b/src/ui/PublishStatusBar.ts index e437ebf8..1f127b77 100644 --- a/src/ui/PublishStatusBar.ts +++ b/src/ui/PublishStatusBar.ts @@ -10,6 +10,7 @@ export class PublishStatusBar { this.numberOfNotesToPublish = numberOfNotesToPublish; this.statusBarItem.createEl("span", { text: "Digital Garden: " }); + this.status = this.statusBarItem.createEl("span", { text: `${this.numberOfNotesToPublish} files marked for publishing`, }); @@ -23,6 +24,7 @@ export class PublishStatusBar { finish(displayDurationMillisec: number) { this.status.innerText = `✅ Published files: ${this.counter}/${this.numberOfNotesToPublish}`; + setTimeout(() => { this.statusBarItem.remove(); }, displayDurationMillisec); diff --git a/src/ui/SettingsModal.ts b/src/ui/SettingsModal.ts index 58032405..86030f43 100644 --- a/src/ui/SettingsModal.ts +++ b/src/ui/SettingsModal.ts @@ -16,6 +16,7 @@ export class UpdateGardenRepositoryModal extends Modal { const text = "Creating PR. This should take about 30-60 seconds"; const loadingText = this.loading?.createEl("h5", { text }); + this.loadingInterval = setInterval(() => { if (loadingText.innerText === `${text}`) { loadingText.innerText = `${text}.`; @@ -40,6 +41,7 @@ export class UpdateGardenRepositoryModal extends Modal { }; const linkText = { text: `${prUrl}`, href: prUrl }; this.progressViewTop.createEl("h2", successmessage); + if (prUrl) { this.progressViewTop.createEl("a", linkText); } @@ -49,6 +51,7 @@ export class UpdateGardenRepositoryModal extends Modal { renderError() { this.loading?.remove(); clearInterval(this.loadingInterval); + const errorMsg = { text: "❌ Something went wrong. Try deleting the branch in GitHub.", attr: {}, diff --git a/src/ui/SettingsView/GithubSettings.ts b/src/ui/SettingsView/GithubSettings.ts index d9484e8f..3b575bdf 100644 --- a/src/ui/SettingsView/GithubSettings.ts +++ b/src/ui/SettingsView/GithubSettings.ts @@ -14,6 +14,7 @@ export class GithubSettings { this.settingsRootElement.id = "github-settings"; this.settingsRootElement.classList.add("settings-tab-content"); this.connectionStatus = "loading"; + this.connectionStatusElement = this.settingsRootElement.createEl( "span", { cls: "connection-status" }, @@ -27,6 +28,7 @@ export class GithubSettings { initializeHeader = () => { this.connectionStatusElement.style.cssText = "margin-left: 10px;"; this.checkConnectionAndSaveSettings(); + const githubSettingsHeader = createEl("h3", { text: "GitHub Authentication (required)", }); @@ -54,6 +56,7 @@ export class GithubSettings { repo: this.settings.settings.githubRepo, }, ); + // If other permissions are needed, add them here and indicate to user on insufficient permissions // Github now advocates for hyper-specific tokens if (response.data.permissions?.admin) { @@ -69,9 +72,11 @@ export class GithubSettings { if (this.connectionStatus === "loading") { this.connectionStatusElement.innerText = "⏳"; } + if (this.connectionStatus === "connected") { this.connectionStatusElement.innerText = "✅"; } + if (this.connectionStatus === "error") { this.connectionStatusElement.innerText = "❌"; } @@ -109,9 +114,11 @@ export class GithubSettings { private initializeGitHubTokenSetting() { const desc = document.createDocumentFragment(); + desc.createEl("span", undefined, (span) => { span.innerText = "A GitHub token with repo permissions. You can generate it "; + span.createEl("a", undefined, (link) => { link.href = "https://github.com/settings/tokens/new?scopes=repo"; diff --git a/src/ui/SettingsView/SettingView.ts b/src/ui/SettingsView/SettingView.ts index bb9ae14e..6cee6f50 100644 --- a/src/ui/SettingsView/SettingView.ts +++ b/src/ui/SettingsView/SettingView.ts @@ -67,15 +67,19 @@ export default class SettingView { async initialize(prModal: Modal) { this.settingsRootElement.empty(); + this.settingsRootElement.createEl("h1", { text: "Digital Garden Settings", }); + const linkDiv = this.settingsRootElement.createEl("div", { attr: { style: "margin-bottom: 10px;" }, }); + linkDiv.createEl("span", { text: "Remember to read the setup guide if you haven't already. It can be found ", }); + linkDiv.createEl("a", { text: "here.", href: "https://dg-docs.ole.dev/getting-started/01-getting-started/", @@ -113,6 +117,7 @@ export default class SettingView { private async initializeDefaultNoteSettings() { const noteSettingsModal = new Modal(this.app); + noteSettingsModal.titleEl.createEl("h1", { text: "Default Note Settings", }); @@ -121,6 +126,7 @@ export default class SettingView { attr: { style: "margin-bottom: 20px; margin-top: -30px;" }, }); linkDiv.createEl("span", { text: "Note Setting Docs is available " }); + linkDiv.createEl("a", { text: "here.", href: "https://dg-docs.ole.dev/getting-started/03-note-settings/", @@ -137,6 +143,7 @@ export default class SettingView { ) .addButton((cb) => { cb.setButtonText("Manage note settings"); + cb.onClick(async () => { noteSettingsModal.open(); }); @@ -149,8 +156,10 @@ export default class SettingView { ) .addToggle((t) => { t.setValue(this.settings.defaultNoteSettings.dgHomeLink); + t.onChange((val) => { this.settings.defaultNoteSettings.dgHomeLink = val; + this.saveSiteSettingsAndUpdateEnv( this.app.metadataCache, this.settings, @@ -166,8 +175,10 @@ export default class SettingView { ) .addToggle((t) => { t.setValue(this.settings.defaultNoteSettings.dgShowLocalGraph); + t.onChange((val) => { this.settings.defaultNoteSettings.dgShowLocalGraph = val; + this.saveSiteSettingsAndUpdateEnv( this.app.metadataCache, this.settings, @@ -183,8 +194,10 @@ export default class SettingView { ) .addToggle((t) => { t.setValue(this.settings.defaultNoteSettings.dgShowBacklinks); + t.onChange((val) => { this.settings.defaultNoteSettings.dgShowBacklinks = val; + this.saveSiteSettingsAndUpdateEnv( this.app.metadataCache, this.settings, @@ -200,8 +213,10 @@ export default class SettingView { ) .addToggle((t) => { t.setValue(this.settings.defaultNoteSettings.dgShowToc); + t.onChange((val) => { this.settings.defaultNoteSettings.dgShowToc = val; + this.saveSiteSettingsAndUpdateEnv( this.app.metadataCache, this.settings, @@ -217,8 +232,10 @@ export default class SettingView { ) .addToggle((t) => { t.setValue(this.settings.defaultNoteSettings.dgShowInlineTitle); + t.onChange((val) => { this.settings.defaultNoteSettings.dgShowInlineTitle = val; + this.saveSiteSettingsAndUpdateEnv( this.app.metadataCache, this.settings, @@ -232,8 +249,10 @@ export default class SettingView { .setDesc("When turned on, a filetree will be shown on your site.") .addToggle((t) => { t.setValue(this.settings.defaultNoteSettings.dgShowFileTree); + t.onChange((val) => { this.settings.defaultNoteSettings.dgShowFileTree = val; + this.saveSiteSettingsAndUpdateEnv( this.app.metadataCache, this.settings, @@ -249,8 +268,10 @@ export default class SettingView { ) .addToggle((t) => { t.setValue(this.settings.defaultNoteSettings.dgEnableSearch); + t.onChange((val) => { this.settings.defaultNoteSettings.dgEnableSearch = val; + this.saveSiteSettingsAndUpdateEnv( this.app.metadataCache, this.settings, @@ -266,8 +287,10 @@ export default class SettingView { ) .addToggle((t) => { t.setValue(this.settings.defaultNoteSettings.dgLinkPreview); + t.onChange((val) => { this.settings.defaultNoteSettings.dgLinkPreview = val; + this.saveSiteSettingsAndUpdateEnv( this.app.metadataCache, this.settings, @@ -283,8 +306,10 @@ export default class SettingView { ) .addToggle((t) => { t.setValue(this.settings.defaultNoteSettings.dgShowTags); + t.onChange((val) => { this.settings.defaultNoteSettings.dgShowTags = val; + this.saveSiteSettingsAndUpdateEnv( this.app.metadataCache, this.settings, @@ -300,8 +325,10 @@ export default class SettingView { ) .addToggle((t) => { t.setValue(this.settings.defaultNoteSettings.dgPassFrontmatter); + t.onChange((val) => { this.settings.defaultNoteSettings.dgPassFrontmatter = val; + this.saveSiteSettingsAndUpdateEnv( this.app.metadataCache, this.settings, @@ -321,6 +348,7 @@ export default class SettingView { .setDesc("Manage themes, sitename and styling on your site") .addButton((cb) => { cb.setButtonText("Manage appearance"); + cb.onClick(async () => { themeModal.open(); }); @@ -337,6 +365,7 @@ export default class SettingView { themeModal.contentEl .createEl("h2", { text: "Style Settings Plugin" }) .prepend(this.getIcon("paintbrush")); + new Setting(themeModal.contentEl) .setName("Apply current style settings to site") .setDesc( @@ -344,21 +373,29 @@ export default class SettingView { ) .addButton((btn) => { btn.setButtonText("Apply"); + btn.onClick(async (_ev) => { new Notice("Applying Style Settings..."); + const styleSettingsNode = document.querySelector( "#css-settings-manager", ); + if (!styleSettingsNode) { new Notice("No Style Settings found"); + return; } + this.settings.styleSettingsCss = styleSettingsNode.innerHTML; + if (!this.settings.styleSettingsCss) { new Notice("No Style Settings found"); + return; } + this.saveSiteSettingsAndUpdateEnv( this.app.metadataCache, this.settings, @@ -398,6 +435,7 @@ export default class SettingView { x.name, ); dd.setValue(this.settings.theme); + dd.onChange(async (val: string) => { this.settings.theme = val; await this.saveSettings(); @@ -412,6 +450,7 @@ export default class SettingView { dd.addOption("dark", "Dark"); dd.addOption("light", "Light"); dd.setValue(this.settings.baseTheme); + dd.onChange(async (val: string) => { this.settings.baseTheme = val; await this.saveSettings(); @@ -440,6 +479,7 @@ export default class SettingView { .addText((tc) => { tc.setPlaceholder("myfavicon.svg"); tc.setValue(this.settings.faviconPath); + tc.onChange(async (val) => { this.settings.faviconPath = val; await this.saveSettings(); @@ -450,6 +490,7 @@ export default class SettingView { themeModal.contentEl .createEl("h2", { text: "Timestamps Settings" }) .prepend(this.getIcon("calendar-clock")); + new Setting(themeModal.contentEl) .setName("Timestamp format") .setDesc( @@ -463,6 +504,7 @@ export default class SettingView { await this.saveSettings(); }), ); + new Setting(themeModal.contentEl) .setName("Show created timestamp") .addToggle((t) => { @@ -516,6 +558,7 @@ export default class SettingView { themeModal.contentEl .createEl("h2", { text: "CSS settings" }) .prepend(this.getIcon("code")); + new Setting(themeModal.contentEl) .setName("Body Classes Key") .setDesc( @@ -533,6 +576,7 @@ export default class SettingView { themeModal.contentEl .createEl("h2", { text: "Note icons Settings" }) .prepend(this.getIcon("image")); + themeModal.contentEl .createEl("div", { attr: { style: "margin-bottom: 10px;" } }) .createEl("a", { @@ -610,6 +654,7 @@ export default class SettingView { new Setting(themeModal.contentEl).addButton((cb) => { cb.setButtonText("Apply settings to site"); + cb.onClick(async (_ev) => { const octokit = new Octokit({ auth: this.settings.githubToken, @@ -623,12 +668,15 @@ export default class SettingView { private async saveSettingsAndUpdateEnv() { const theme = JSON.parse(this.settings.theme); const baseTheme = this.settings.baseTheme; + if (theme.modes.indexOf(baseTheme) < 0) { new Notice( `The ${theme.name} theme doesn't support ${baseTheme} mode.`, ); + return; } + const gardenManager = new DigitalGardenSiteManager( this.app.metadataCache, this.settings, @@ -644,6 +692,7 @@ export default class SettingView { saveSettings: () => Promise, ) { let updateFailed = false; + try { const gardenManager = new DigitalGardenSiteManager( metadataCache, @@ -664,12 +713,15 @@ export default class SettingView { private async addFavicon(octokit: Octokit) { let base64SettingsFaviconContent = ""; + if (this.settings.faviconPath) { const faviconFile = this.app.vault.getAbstractFileByPath( this.settings.faviconPath, ); + if (!(faviconFile instanceof TFile)) { new Notice(`${this.settings.faviconPath} is not a valid file.`); + return; } const faviconContent = await this.app.vault.readBinary(faviconFile); @@ -691,6 +743,7 @@ export default class SettingView { let faviconExists = true; let faviconsAreIdentical = false; let currentFaviconOnSite = null; + try { currentFaviconOnSite = await octokit.request( "GET /repos/{owner}/{repo}/contents/{path}", @@ -700,6 +753,7 @@ export default class SettingView { path: "src/site/favicon.svg", }, ); + faviconsAreIdentical = // @ts-expect-error TODO: abstract octokit response currentFaviconOnSite.data.content @@ -734,6 +788,7 @@ export default class SettingView { .setValue(this.settings.gardenBaseUrl) .onChange(async (value) => { this.settings.gardenBaseUrl = value; + this.debouncedSaveAndUpdate( this.app.metadataCache, this.settings, @@ -761,6 +816,7 @@ export default class SettingView { private initializePathRewriteSettings() { const rewriteRulesModal = new Modal(this.app); + rewriteRulesModal.titleEl.createEl("h1", { text: "Path Rewrite Rules", }); @@ -773,6 +829,7 @@ export default class SettingView { ) .addButton((cb) => { cb.setButtonText("Manage Rewrite Rules"); + cb.onClick(() => { rewriteRulesModal.open(); }); @@ -794,18 +851,22 @@ export default class SettingView { const list = rewriteSettingContainer.createEl("ol"); list.createEl("li", { text: `One rule-per line` }); + list.createEl("li", { text: `The format is [from_vault_path]:[to_garden_path]`, }); list.createEl("li", { text: `Matching will exit on first match` }); + rewriteSettingContainer.createEl("div", { text: `Example: If you want the vault folder "Personal/Journal" to be shown as only "Journal" in the left file sidebar in the garden, add the line "Personal/Journal:Journal"`, attr: { class: "setting-item-description" }, }); + rewriteSettingContainer.createEl("div", { text: `Note: rewriting a folder to the base path "[from_vault_path]:" is not supported at the moment.`, attr: { class: "setting-item-description" }, }); + rewriteSettingContainer.createEl("div", { text: `Any affected notes will show up as changed in the publication center`, attr: { class: "setting-item-description" }, @@ -817,6 +878,7 @@ export default class SettingView { field.setPlaceholder("Personal/Journal:Journal"); field.inputEl.rows = 5; field.inputEl.cols = 100; + field .setValue(this.settings.pathRewriteRules) .onChange(async (value) => { @@ -847,6 +909,7 @@ export default class SettingView { previewInput.addEventListener("input", () => { const testPath = previewInput.value; + const rewriteTestResult = getGardenPathForNote( testPath, getRewriteRules(this.settings.pathRewriteRules), @@ -872,6 +935,7 @@ export default class SettingView { ) .addButton((cb) => { cb.setButtonText("Manage Custom Filters"); + cb.onClick(() => { customFilterModal.open(); }); @@ -886,12 +950,15 @@ export default class SettingView { }, }, ); + rewriteSettingsContainer.createEl( "div", ).innerHTML = `Define regex filters to replace note content before publishing.`; + rewriteSettingsContainer.createEl("div", { attr: { class: "setting-item-description" }, }).innerHTML = `Format: [regex pattern, replacement, regex flags]`; + rewriteSettingsContainer.createEl("div", { attr: { class: "setting-item-description", @@ -900,27 +967,33 @@ export default class SettingView { }).innerHTML = `Example: filter [:smile:, 😀, g] will replace text with real emojis`; const customFilters = this.settings.customFilters; + new Setting(rewriteSettingsContainer) .setName("Filters") .addButton((button) => { button.setButtonText("Add"); button.setTooltip("Add a filter"); button.setIcon("plus"); + button.onClick(async () => { const customFilters = this.settings.customFilters; + customFilters.push({ pattern: "", flags: "g", replace: "", }); filterList.empty(); + for (let i = 0; i < customFilters.length; i++) { addFilterInput(customFilters[i], filterList, i, this); } }); }); + const filterList = rewriteSettingsContainer.createDiv("custom-filter-list"); + for (let i = 0; i < customFilters.length; i++) { addFilterInput(customFilters[i], filterList, i, this); } @@ -933,6 +1006,7 @@ export default class SettingView { this.settingsRootElement .createEl("h3", { text: "Update site" }) .prepend(getIcon("sync") ?? ""); + new Setting(this.settingsRootElement) .setName("Site Template") .setDesc( @@ -940,11 +1014,13 @@ export default class SettingView { ) .addButton((button) => { button.setButtonText("Manage site template"); + button.onClick(() => { modal.open(); }); }); modal.titleEl.createEl("h2", { text: "Update site" }); + new Setting(modal.contentEl) .setName("Update site to latest template") .setDesc( @@ -962,6 +1038,7 @@ export default class SettingView { this.settingsRootElement .createEl("h3", { text: "Support" }) .prepend(this.getIcon("heart")); + this.settingsRootElement .createDiv({ attr: { @@ -983,11 +1060,13 @@ export default class SettingView { if (previousPrUrls.length === 0) { return; } + const header = modal.contentEl.createEl("h2", { text: "➕ Recent Pull Request History", }); const prsContainer = modal.contentEl.createEl("ul", {}); prsContainer.hide(); + header.onClickEvent(() => { if (prsContainer.isShown()) { prsContainer.hide(); @@ -997,6 +1076,7 @@ export default class SettingView { header.textContent = "➖ Recent Pull Request History"; } }); + previousPrUrls.map((prUrl) => { const li = prsContainer.createEl("li", { attr: { style: "margin-bottom: 10px" }, diff --git a/src/ui/SettingsView/addFilterInput.ts b/src/ui/SettingsView/addFilterInput.ts index e7c10f7b..4fa36e24 100644 --- a/src/ui/SettingsView/addFilterInput.ts +++ b/src/ui/SettingsView/addFilterInput.ts @@ -13,6 +13,7 @@ function addFilterInput( }, }); const patternField = new TextComponent(el); + patternField .setPlaceholder("regex pattern") .setValue(filter.pattern) @@ -28,6 +29,7 @@ function addFilterInput( item.appendChild(patternEl); const replaceField = new TextComponent(el); + replaceField .setPlaceholder("replacement") .setValue(filter.replace) @@ -44,6 +46,7 @@ function addFilterInput( item.appendChild(replaceEl); const flagField = new TextComponent(el); + flagField .setPlaceholder("flags") .setValue(filter.flags) @@ -62,9 +65,11 @@ function addFilterInput( const removeButton = new ButtonComponent(el); removeButton.setIcon("minus"); removeButton.setTooltip("Remove filter"); + removeButton.onClick(async () => { plugin.settings.customFilters.splice(idx, 1); el.empty(); + for (let i = 0; i < plugin.settings.customFilters.length; i++) { addFilterInput(plugin.settings.customFilters[i], el, i, plugin); } diff --git a/src/ui/suggest/suggest.ts b/src/ui/suggest/suggest.ts index 32b083a8..4b8200a4 100644 --- a/src/ui/suggest/suggest.ts +++ b/src/ui/suggest/suggest.ts @@ -26,6 +26,7 @@ class Suggest { ".suggestion-item", this.onSuggestionClick.bind(this), ); + containerEl.on( "mousemove", ".suggestion-item", @@ -35,6 +36,7 @@ class Suggest { scope.register([], "ArrowUp", (event) => { if (!event.isComposing) { this.setSelectedItem(this.selectedItem - 1, true); + return false; } }); @@ -42,6 +44,7 @@ class Suggest { scope.register([], "ArrowDown", (event) => { if (!event.isComposing) { this.setSelectedItem(this.selectedItem + 1, true); + return false; } }); @@ -49,6 +52,7 @@ class Suggest { scope.register([], "Enter", (event) => { if (!event.isComposing) { this.useSelectedItem(event); + return false; } }); @@ -84,6 +88,7 @@ class Suggest { useSelectedItem(event: MouseEvent | KeyboardEvent) { const currentValue = this.values[this.selectedItem]; + if (currentValue) { this.owner.selectSuggestion(currentValue, event); } @@ -131,6 +136,7 @@ export abstract class TextInputSuggest implements ISuggestOwner { this.inputEl.addEventListener("input", this.onInputChanged.bind(this)); this.inputEl.addEventListener("focus", this.onInputChanged.bind(this)); this.inputEl.addEventListener("blur", this.close.bind(this)); + this.suggestEl.on( "mousedown", ".suggestion-container", @@ -156,6 +162,7 @@ export abstract class TextInputSuggest implements ISuggestOwner { (this.app).keymap.pushScope(this.scope); container.appendChild(this.suggestEl); + this.popper = createPopper(inputEl, this.suggestEl, { placement: "bottom-start", modifiers: [ @@ -168,6 +175,7 @@ export abstract class TextInputSuggest implements ISuggestOwner { // second pass - position it with the width bound to the reference element // we need to early exit to avoid an infinite loop const targetWidth = `${state.rects.reference.width}px`; + if (state.styles.popper.width === targetWidth) { return; } diff --git a/src/utils/markdown.ts b/src/utils/markdown.ts index 7579f807..7a7f403f 100644 --- a/src/utils/markdown.ts +++ b/src/utils/markdown.ts @@ -6,11 +6,13 @@ const seperateHashesFromHeader = ( if (matches?.groups) { const { hashes, _space, title } = matches.groups; + return { hashes, title, }; } + // always return one hash for valid md heading return { hashes: "#", title: rawHeading.trim() }; }; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index eb3adf3d..378a3dd5 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -9,9 +9,11 @@ function arrayBufferToBase64(buffer: ArrayBuffer) { let binary = ""; const bytes = new Uint8Array(buffer); const len = bytes.byteLength; + for (let i = 0; i < len; i++) { binary += String.fromCharCode(bytes[i]); } + return Base64.btoa(binary); } @@ -81,13 +83,16 @@ function getGardenPathForNote( for (const { from, to } of rules) { if (vaultPath.startsWith(from)) { const newPath = vaultPath.replace(from, to); + // remote leading slash if to = "" if (newPath.startsWith("/")) { return newPath.replace("/", ""); } + return newPath; } } + return vaultPath; } @@ -98,9 +103,11 @@ function escapeRegExp(string: string) { function fixSvgForXmlSerializer(svgElement: SVGSVGElement): void { // Insert a comment in the style tags to prevent XMLSerializer from self-closing it during serialization. const styles = svgElement.getElementsByTagName("style"); + if (styles.length > 0) { for (let i = 0; i < styles.length; i++) { const style = styles[i]; + if (!style.textContent?.trim()) { style.textContent = "/**/"; } @@ -112,9 +119,11 @@ function sanitizePermalink(permalink: string): string { if (!permalink.endsWith("/")) { permalink += "/"; } + if (!permalink.startsWith("/")) { permalink = "/" + permalink; } + return permalink; }