From c14c231d3445b02eec4ad3f96df2f7791008e316 Mon Sep 17 00:00:00 2001 From: Inhji Date: Sun, 11 Jun 2023 17:16:06 +0200 Subject: [PATCH] add support for writing back url into frontmatter after publish --- src/MicroPlugin.ts | 23 ++- src/MicroPluginContainer.ts | 1 + src/factories/ServiceFactory.ts | 19 +++ src/factories/ViewModelFactory.ts | 25 +++- src/networking/NetworkRequestFactory.ts | 6 +- src/services/FrontmatterService.ts | 185 ++++++++++++++++++++++++ src/views/PublishViewModel.ts | 13 +- 7 files changed, 257 insertions(+), 15 deletions(-) create mode 100644 src/services/FrontmatterService.ts diff --git a/src/MicroPlugin.ts b/src/MicroPlugin.ts index 40e7d05..f3039f3 100644 --- a/src/MicroPlugin.ts +++ b/src/MicroPlugin.ts @@ -1,14 +1,16 @@ import { ErrorView } from '@views/ErrorView' -import { Notice, Plugin } from 'obsidian' +import { Editor, Notice, Plugin } from 'obsidian' import { MicroPluginContainerInterface, MicroPluginContainer } from '@base/MicroPluginContainer' import { MicroPluginSettingsView } from '@views/MicroPluginSettingsView' import { PublishView } from '@views/PublishView' import { ServiceFactory, ServiceFactoryInterface } from '@factories/ServiceFactory' import { StoredSettings, defaultSettings } from '@stores/StoredSettings' -import { TagSynchronizationServiceInterface } from '@services/TagSynchronizationService' +import { TagSynchronizationServiceDelegate, TagSynchronizationServiceInterface } from '@services/TagSynchronizationService' import { ViewModelFactoryInterface, ViewModelFactory } from '@factories/ViewModelFactory' +import { FrontmatterServiceDelegate, FrontmatterServiceInterface } from './services/FrontmatterService' +import { PublishResponse } from './networking/PublishResponse' -export default class MicroPlugin extends Plugin { +export default class MicroPlugin extends Plugin implements TagSynchronizationServiceDelegate { // Properties @@ -17,15 +19,17 @@ export default class MicroPlugin extends Plugin { private viewModelFactory: ViewModelFactoryInterface private serviceFactory: ServiceFactoryInterface private synchronizationService: TagSynchronizationServiceInterface + private frontmatterService: FrontmatterServiceInterface // Public public async onload() { await this.loadSettings() await this.loadDependencies() - await this.loadViewModelFactory() await this.loadServiceFactory() await this.registerSynchronizationService() + await this.registerFrontmatterService() + await this.loadViewModelFactory() this.synchronizationService.fetchTags() @@ -41,7 +45,8 @@ export default class MicroPlugin extends Plugin { new PublishView( this.viewModelFactory.makePublishViewModel( markdownView.file.basename, - editor.getValue() + editor.getValue(), + editor ) ).open() } @@ -88,7 +93,8 @@ export default class MicroPlugin extends Plugin { private async loadViewModelFactory() { this.viewModelFactory = new ViewModelFactory( - this.container + this.container, + this.frontmatterService ) } @@ -105,6 +111,11 @@ export default class MicroPlugin extends Plugin { ) } + private async registerFrontmatterService() { + this.frontmatterService = this.serviceFactory + .makeFrontmatterService() + } + // TagSynchronizationServiceDelegate public tagSynchronizationDidSucceed( diff --git a/src/MicroPluginContainer.ts b/src/MicroPluginContainer.ts index cce5d85..8e7dfbd 100644 --- a/src/MicroPluginContainer.ts +++ b/src/MicroPluginContainer.ts @@ -17,6 +17,7 @@ export interface MicroPluginContainerInterface { // The network request factory, used to build the // requests which will be executed by the network client. networkRequestFactory: NetworkRequestFactoryInterface + } export class MicroPluginContainer implements MicroPluginContainerInterface { diff --git a/src/factories/ServiceFactory.ts b/src/factories/ServiceFactory.ts index 7566be2..7709b53 100644 --- a/src/factories/ServiceFactory.ts +++ b/src/factories/ServiceFactory.ts @@ -1,4 +1,8 @@ import { MicroPluginContainerInterface } from "@base/MicroPluginContainer"; +import { + FrontmatterService, + FrontmatterServiceInterface, + FrontmatterServiceDelegate } from "@base/services/FrontmatterService"; import { TagSynchronizationServiceInterface, TagSynchronizationService, @@ -13,6 +17,13 @@ export interface ServiceFactoryInterface { makeTagSynchronizationService( delegate?: TagSynchronizationServiceDelegate ): TagSynchronizationServiceInterface + + // Builds the yaml frontmatter service, used by the client + // to update the frontmatter of the file after publishing + // with publish date, used tags, etc. + makeFrontmatterService( + delegate?: FrontmatterServiceDelegate + ): FrontmatterServiceInterface } /* @@ -47,4 +58,12 @@ export class ServiceFactory implements ServiceFactoryInterface { delegate ) } + + public makeFrontmatterService( + delegate?: FrontmatterServiceDelegate + ): FrontmatterServiceInterface { + return new FrontmatterService( + delegate + ) + } } \ No newline at end of file diff --git a/src/factories/ViewModelFactory.ts b/src/factories/ViewModelFactory.ts index 0d42593..fe57544 100644 --- a/src/factories/ViewModelFactory.ts +++ b/src/factories/ViewModelFactory.ts @@ -1,8 +1,11 @@ import { MicroPluginSettingsViewModel } from '@views/MicroPluginSettingsViewModel' -import { PublishViewModel } from '@views/PublishViewModel' +import { PublishViewModel, PublishViewModelDelegate } from '@views/PublishViewModel' import { TagSuggestionViewModel, TagSuggestionDelegate } from '@views/TagSuggestionViewModel' import { ErrorViewModel } from '@views/ErrorViewModel' import { MicroPluginContainerInterface } from '@base/MicroPluginContainer' +import { Editor } from 'obsidian' +import { ServiceFactoryInterface } from './ServiceFactory' +import { FrontmatterServiceInterface } from '@base/services/FrontmatterService' export interface ViewModelFactoryInterface { @@ -10,7 +13,9 @@ export interface ViewModelFactoryInterface { // to Micro.blog via the Commands Palette. makePublishViewModel( title: string, - content: string + content: string, + editor: Editor, + delegate?: PublishViewModelDelegate ): PublishViewModel // Builds the Plugin Settings View Model, used by the plugin @@ -37,22 +42,26 @@ export class ViewModelFactory implements ViewModelFactoryInterface { // Properties private container: MicroPluginContainerInterface + private frontmatterService: FrontmatterServiceInterface // Life cycle constructor( - container: MicroPluginContainerInterface + container: MicroPluginContainerInterface, + frontmatterService: FrontmatterServiceInterface ) { this.container = container + this.frontmatterService = frontmatterService } // Public public makePublishViewModel( title: string, - content: string + content: string, + editor: Editor ): PublishViewModel { - return new PublishViewModel( + const viewModel = new PublishViewModel( title, content, this.container.settings.defaultTags, @@ -61,8 +70,12 @@ export class ViewModelFactory implements ViewModelFactoryInterface { this.container.settings.selectedBlogID, this.container.networkClient, this.container.networkRequestFactory, - this + this, + this.frontmatterService, + editor ) + + return viewModel } public makeMicroPluginSettingsViewModel(): MicroPluginSettingsViewModel { diff --git a/src/networking/NetworkRequestFactory.ts b/src/networking/NetworkRequestFactory.ts index 869a5e6..1a5469e 100644 --- a/src/networking/NetworkRequestFactory.ts +++ b/src/networking/NetworkRequestFactory.ts @@ -1,4 +1,5 @@ import { NetworkRequest } from '@networking/NetworkRequest' +import { log } from 'console' export interface NetworkRequestFactoryInterface { @@ -78,8 +79,9 @@ export class NetworkRequestFactory implements NetworkRequestFactoryInterface { return { url: this.endpointUrl(), - parameters: parameters, - method: 'POST' + parameters: new URLSearchParams(), + method: 'POST', + body: parameters.toString() } } diff --git a/src/services/FrontmatterService.ts b/src/services/FrontmatterService.ts new file mode 100644 index 0000000..783721a --- /dev/null +++ b/src/services/FrontmatterService.ts @@ -0,0 +1,185 @@ +import { PublishResponse } from "@base/networking/PublishResponse"; +import { Editor, EditorPosition, parseYaml, stringifyYaml } from "obsidian"; + +export const YAML_REGEX = /^---\n(?:((?:.|\n)*?)\n)?---(?=\n|$)/; + +type CommonUpdateParam = { + editor: Editor; + key: string; + value: string; + action: 'replace' | 'insert' | 'remove'; +} + +type BulkUpdateParam = { + editor: Editor; + updateDatas: Record; + removeDatas: string[]; + action: 'bulk' +} + +export interface FrontmatterServiceInterface { + updateFrontmatter(response: PublishResponse, editor: Editor): void +} + +export interface FrontmatterServiceDelegate { + frontmatterUpdateDidSucceed(): void + + frontmatterUpdateDidFail(error: Error): void +} + +export class FrontmatterService implements FrontmatterServiceInterface { + + // Properties + + private delegate?: FrontmatterServiceDelegate + + // Life cycle + + constructor(delegate?: FrontmatterServiceDelegate) { + this.delegate = delegate + } + + // Public + + public async updateFrontmatter(response: PublishResponse, editor: Editor) { + const yaml = this.getObjectYaml(editor) + + this.upsert("url", response.url, false, editor) + this.delegate?.frontmatterUpdateDidSucceed() + } + + + // Get objectify yaml of current file + getObjectYaml(editor: Editor) { + const stringYaml = this.getYaml(editor); + + return stringYaml? parseYaml(stringYaml.slice(4, -4)): {} + } + + // Exchange item position in array + itemMove(arr: T[], itemIdx1: number, itemIdx2: number): void { + [arr[itemIdx1], arr[itemIdx2]] = [arr[itemIdx2], arr[itemIdx1]]; + } + + // Add item in specific position + itemAdd(arr: T[], itemIdx: number, item: T): void { + arr.splice(itemIdx, 0, item) + } + + // Delete specific item in array + itemDelete(arr: T[], itemIndex: number): void { + arr.splice(itemIndex, 1); + } + + // Get yaml section + getYaml(editor: Editor): string { + const matchResult = editor.getValue().match(YAML_REGEX); + + return matchResult?.[0] ?? ''; + } + + generateActionKeyword(data: CommonUpdateParam | BulkUpdateParam) { + const {editor, action} = data; + const yamlSection = this.getYaml(editor); + const yaml = yamlSection.slice(4, -3); + const objectYaml = this.getObjectYaml(editor); + const objectSnippet: Record = {}; + + if (action === 'replace') objectSnippet[data.key] = data.value; + + if (action === 'insert') { + if (objectYaml[data.key] instanceof Array) { + console.log('inserting into array') + objectSnippet[data.key] = [...objectYaml[data.key], data.value]; + } else { + console.log('inserting') + //objectSnippet[data.key] = [data.value]; + objectSnippet[data.key] = data.value; + } + } + if (action === 'remove') { + if (objectYaml[data.key] instanceof Array) { + const newValue = objectYaml[data.key].filter((val: string) => val !== data.value); + objectSnippet[data.key] = newValue.length? newValue: null; + } else { + objectSnippet[data.key] = null; + } + } + + if (action === 'bulk') { + Object.entries(data.updateDatas).forEach(([key, value]) => objectSnippet[key] = value ); + data.removeDatas.forEach((key) => objectSnippet[key] = null) + } + + let replacement = `---\n${this.generateReplacement(yaml, objectSnippet)}---`; + const startPosition: EditorPosition = {line: 0, ch: 0}; + const endPosition: EditorPosition = editor.offsetToPos(yamlSection.length); + + // Make sure there is at least one newline character + // after the end of the frontmatter + const charAfterYaml = editor.getRange(endPosition, {ch: endPosition.ch + 1, line: endPosition.line}) + replacement = charAfterYaml == "\n" ? replacement : replacement + "\n" + + return {replacement, startPosition, endPosition} + } + + generateReplacement(yaml: string, snippet: Record) { + return Object.entries(snippet).reduce((temp, [key, value]) => { + const YAML_FIELD_REGEX = new RegExp(`(${key} *:).+?\\n(?=\\S|$)`, 'gs'); + + const replacement = (value === null)? '': stringifyYaml({[key]: value}); + + return temp.match(YAML_FIELD_REGEX)? temp.replace(YAML_FIELD_REGEX, replacement): `${temp}${replacement}`; + }, yaml) + } + + flatYamlFields(yaml: string, flatFields: string[]): string { + const objectYaml = parseYaml(yaml.slice(4, -4)); + + return flatFields.reduce((temp, key) => { + const YAML_FIELD_REGEX = new RegExp(`(${key}:).+?(?=\\n\\S|$)`, 'gs'); + + return temp.match(YAML_FIELD_REGEX)? temp.replace(YAML_FIELD_REGEX, `$1 [${objectYaml[key].join(', ')}]`): temp; + }, yaml) + } + + replace(key: string, value: string, editor: Editor): void { + const {replacement, startPosition, endPosition} = this.generateActionKeyword({key, value, editor, action: 'replace'}); + + editor.replaceRange(replacement, startPosition, endPosition) + } + + insert(key: string, value: string, flat: boolean, editor: Editor): void { + const {replacement, startPosition, endPosition} = this.generateActionKeyword({key, value, editor, action: 'insert'}); + + const postProcessedReplacement = flat? this.flatYamlFields(replacement, [key]): replacement; + + editor.replaceRange(postProcessedReplacement, startPosition, endPosition) + } + + upsert(key: string, value: string, flat: boolean, editor: Editor): void { + const yaml = this.getObjectYaml(editor) + + if (yaml[key]) { + this.replace(key, value, editor) + } else { + this.insert(key, value, flat, editor) + } + } + + remove(key: string, value: string, flat: boolean, editor: Editor): void { + const {replacement, startPosition, endPosition} = this.generateActionKeyword({key, value, editor, action: 'remove'}); + + const postProcessedReplacement = flat? this.flatYamlFields(replacement, [key]): replacement; + + editor.replaceRange(postProcessedReplacement, startPosition, endPosition) + } + + bulkUpdate(updateDatas: Record, removeDatas: string[], flatFields: string[], editor: Editor): void { + const {replacement, startPosition, endPosition} = this.generateActionKeyword({updateDatas, removeDatas, editor, action: 'bulk'}); + + const flattedReplacement = this.flatYamlFields(replacement, flatFields); + + editor.replaceRange(flattedReplacement, startPosition, endPosition); + } +} \ No newline at end of file diff --git a/src/views/PublishViewModel.ts b/src/views/PublishViewModel.ts index b6f58a9..10df91e 100644 --- a/src/views/PublishViewModel.ts +++ b/src/views/PublishViewModel.ts @@ -3,6 +3,9 @@ import { NetworkClientInterface } from '@networking/NetworkClient' import { PublishResponse } from '@networking/PublishResponse' import { TagSuggestionDelegate, TagSuggestionViewModel } from '@views/TagSuggestionViewModel' import { ViewModelFactoryInterface } from '@factories/ViewModelFactory' +import { Editor } from 'obsidian' +import { ServiceFactoryInterface } from '@base/factories/ServiceFactory' +import { FrontmatterService, FrontmatterServiceInterface } from '@base/services/FrontmatterService' /* * Publish View Delegate Interface, implemented by @@ -53,6 +56,8 @@ export class PublishViewModel implements TagSuggestionDelegate { private networkClient: NetworkClientInterface private networkRequestFactory: NetworkRequestFactoryInterface private viewModelFactory: ViewModelFactoryInterface + private frontmatterService: FrontmatterServiceInterface + private editor: Editor readonly blogs: Record // Life cycle @@ -66,7 +71,9 @@ export class PublishViewModel implements TagSuggestionDelegate { selectedBlogID: string, networkClient: NetworkClientInterface, networkRequestFactory: NetworkRequestFactoryInterface, - viewModelFactory: ViewModelFactoryInterface + viewModelFactory: ViewModelFactoryInterface, + frontmatterService: FrontmatterServiceInterface, + editor: Editor ) { this.titleWrappedValue = title this.content = content @@ -80,6 +87,8 @@ export class PublishViewModel implements TagSuggestionDelegate { this.networkClient = networkClient this.networkRequestFactory = networkRequestFactory this.viewModelFactory = viewModelFactory + this.frontmatterService = frontmatterService + this.editor = editor } // Public @@ -162,6 +171,8 @@ export class PublishViewModel implements TagSuggestionDelegate { request ) + this.frontmatterService.updateFrontmatter(response, this.editor) + this.delegate?.publishDidSucceed(response) } catch (error) { this.delegate?.publishDidFail(error)