From 7f1575a8499bc2d452aba79251b77cd6c4e288a4 Mon Sep 17 00:00:00 2001 From: Cleon <18450687+meld-cp@users.noreply.github.com> Date: Thu, 5 Jan 2023 13:34:44 +1300 Subject: [PATCH] wip --- README.md | 1 + esbuild.config.mjs | 2 +- main.ts | 53 +++++++-- src/code-block-info.ts | 8 ++ src/compiler.ts | 26 +++-- src/data-set.ts | 2 +- src/named-code-block.ts | 14 +++ src/parser.ts | 230 ++++++++++++++++++++++++++-------------- src/run-context.ts | 5 +- tsconfig.json | 9 +- 10 files changed, 248 insertions(+), 102 deletions(-) create mode 100644 src/code-block-info.ts create mode 100644 src/named-code-block.ts diff --git a/README.md b/README.md index ec9cd5d..c3250e4 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Write and execute JavaScript in code blocks to render templates, query DataView - [ ] better docs and examples - [ ] alpha release - [ ] test on mobile +- [ ] better file logging ## Manually installing the plugin diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 2aec684..43abbe5 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -35,7 +35,7 @@ esbuild.build({ ...builtins], format: 'cjs', watch: !prod, - target: 'es2018', + target: 'es2021', logLevel: "info", sourcemap: prod ? false : 'inline', treeShaking: true, diff --git a/main.ts b/main.ts index 423dec4..4aa4918 100644 --- a/main.ts +++ b/main.ts @@ -11,12 +11,19 @@ const DEFAULT_SETTINGS: MeldBuildPluginSettings = { mySetting: 'default' } +const CODE_BLOCK_LANG_TOOLBAR = 'meld-build-toolbar'; + export default class MeldBuildPlugin extends Plugin { settings: MeldBuildPluginSettings; + async onload() { + + //console.debug((app as any).commands.commands); + await this.loadSettings(); + await this.reloadActiveViewsWithToolbars(); this.registerHandlebarHelpers(); @@ -27,7 +34,7 @@ export default class MeldBuildPlugin extends Plugin { // }); // }); - this.registerMarkdownCodeBlockProcessor('meld-build-toolbar', (source, el, ctx) => { + this.registerMarkdownCodeBlockProcessor( CODE_BLOCK_LANG_TOOLBAR, (source, el, ctx) => { const lines = source.split('\n'); const valueMap = new Map(); lines.forEach(line => { @@ -44,14 +51,8 @@ export default class MeldBuildPlugin extends Plugin { const showHelpButton = helpButtonLabel !== ''; if (showRunButton){ - el.createEl('button', { text: runButtonLabel ?? 'Run'}, el =>{ - el.on('click', '*', ev=>{ - const view = app.workspace.getActiveViewOfType( MarkdownView ); - if (!view){ - return; - } - this.buildAndRun(view.editor, view); - }); + el.createEl('button', { text: runButtonLabel ?? 'Run'}, el => { + el.on('click', '*', ev => this.buildAndRunActiveView() ); }); } @@ -75,7 +76,37 @@ export default class MeldBuildPlugin extends Plugin { } - private buildAndRun( editor:Editor, view: MarkdownView ){ + private async reloadActiveViewsWithToolbars(){ + app.workspace.iterateAllLeaves( leaf =>{ + const view = leaf.view; + if ( view instanceof MarkdownView ){ + + if (!view.editor.getValue().contains(CODE_BLOCK_LANG_TOOLBAR)){ + return; + } + + console.debug( `Meld-Build::Rebuilding view for file '${view.file.path}'` ); + + (view.leaf as any).rebuildView(); + } + }); + //const view = app.workspace.getActiveViewOfType( MarkdownView ); + //if (view == null){ + // return; + //} + + + } + + private async buildAndRunActiveView(){ + const view = app.workspace.getActiveViewOfType( MarkdownView ); + if (!view){ + return; + } + await this.buildAndRun( view.editor, view); + } + + private async buildAndRun( editor:Editor, view: MarkdownView ){ const logger = new RunLogger(); try{ //await view.save(); @@ -83,7 +114,7 @@ export default class MeldBuildPlugin extends Plugin { const runner = compiler.compile(logger, editor, view); runner(); }catch(e){ - logger.error(e); + await logger.error(e); new Notice(e); } } diff --git a/src/code-block-info.ts b/src/code-block-info.ts new file mode 100644 index 0000000..f14a279 --- /dev/null +++ b/src/code-block-info.ts @@ -0,0 +1,8 @@ +export class CodeBlockInfo{ + public language:string; + public params:string[]; + constructor( language: string, params?:string[] ){ + this.language = language; + this.params = params ?? []; + } +} \ No newline at end of file diff --git a/src/compiler.ts b/src/compiler.ts index 3ce5b21..379f2d3 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -8,6 +8,7 @@ import { DataSet, IDataSetCollection } from "src/data-set"; import { Parser } from "src/parser"; import { RunLogger } from 'src/run-logger'; import { TRunContext } from "src/run-context"; +import { NamedCodeBlock } from "./named-code-block"; export class Compiler{ private templateLanguages = ['html', 'css']; @@ -23,6 +24,8 @@ export class Compiler{ // build context const context = this.build_run_context(logger, editor, fileCache, view); + //console.debug(context.sourceCode); + // return runner return () => this.buildSandboxedRunnerFunction(context); //return () => this.buildRunnerFunction(sourceCode, context); @@ -45,8 +48,14 @@ export class Compiler{ // templates const templateBlocks = pzr.fetchCodeBlocks( editor, fileCache, this.templateLanguages ); + + // source code + const sourceCode = codeBlocks.map( e=>e.content ).join('\n'); + + //console.debug({sourceCode}); + return { - sourceCode: codeBlocks.join('\n'), + sourceCode: sourceCode, data: data, templates: templateBlocks, @@ -54,10 +63,15 @@ export class Compiler{ logger : log, log: async (...x) => await log.info( ...x ), - render( template, data ) { - const templateBuilder = HB.compile( template ); - const result = templateBuilder(data); - return result; + render( template:string|NamedCodeBlock, data:any ) { + + if( typeof template == 'string' ){ + return HB.compile( template )(data); + }else if ( template instanceof NamedCodeBlock ){ + return HB.compile( template.content )(data); + } + + return ''; }, ui: { @@ -255,7 +269,7 @@ export class Compiler{ private async import( log: RunLogger, data: IDataSetCollection, - templates: string[], + templates: NamedCodeBlock[], path:string ) : Promise{ diff --git a/src/data-set.ts b/src/data-set.ts index c19228f..4938c07 100644 --- a/src/data-set.ts +++ b/src/data-set.ts @@ -1,5 +1,5 @@ export class DataSet extends Array { - // TODO: is this needed? + } export interface IDataSetCollection { diff --git a/src/named-code-block.ts b/src/named-code-block.ts new file mode 100644 index 0000000..e8268bb --- /dev/null +++ b/src/named-code-block.ts @@ -0,0 +1,14 @@ +import { CodeBlockInfo } from "./code-block-info"; + +export class NamedCodeBlock{ + + public info:CodeBlockInfo; + public name:string; + public content:string; + + constructor( info:CodeBlockInfo, name:string, content:string ){ + this.info = info, + this.name = name; + this.content = content; + } +} \ No newline at end of file diff --git a/src/parser.ts b/src/parser.ts index f90f4ba..e33c3f7 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,14 +1,136 @@ import { DataSet, DataSetRow, IDataSetCollection } from "src/data-set"; import { CachedMetadata, Editor } from "obsidian"; +import { NamedCodeBlock } from "./named-code-block"; +import { CodeBlockInfo } from "./code-block-info"; export class Parser { + public fetchCodeBlocks( + editor: Editor, + fileCache: CachedMetadata | undefined, + languages :string[] + ): NamedCodeBlock[] { + const result: NamedCodeBlock[] = []; + + if ( fileCache == undefined ){ + return result; + } + + if ( fileCache.sections == undefined ){ + return result; + } + + //console.log(fileCache.sections); + const codeBlocks = fileCache.sections?.filter( s => ['heading','code'].contains( s.type ) ) ?? []; + //console.debug({codeBlocks}); + + let cbName = ''; + codeBlocks.forEach( codeBlockSection => { + + const from = editor.offsetToPos( codeBlockSection.position.start.offset ); + const to = editor.offsetToPos( codeBlockSection.position.end.offset ); + const content = editor.getRange( from, to ); + + //console.debug(content); + + if ( codeBlockSection.type == 'heading' ){ + cbName = this.extractHeaderName(content); + return; + } + + // code block + const lines = content.split('\n'); + if ( lines.length <= 2 ){ + return; + } + + // filter languages + const codeBlockInfo = this.extractCodeBlockInfo(lines[0]); + if (!codeBlockInfo){ + return; + } + + if ( !languages.contains( codeBlockInfo.language ) ){ + return; + } + + // remove first and last lines + const cbContent = lines.slice(1,-1).join('\n'); + + const codeBlock = new NamedCodeBlock( codeBlockInfo, cbName, cbContent ); + + result.push( codeBlock ); + + }); + + return result; + } + + private extractCodeBlockInfo( line:string ) : CodeBlockInfo | null { + /* + match tests + ```lang => { language: 'lang', params:[] } + ```lang with space => { language: 'lang', params:['with','space'] } + ``` whitespaced => { language: 'whitespaced', params:[] } + ```` multitick => { language: 'multitick', params:[] } + ````multitick with space => { language: 'multitick', params:['with','space'] } + */ + const matches = line.match(/````*\s*([a-z0-9-]+)(?:\s*([a-z0-9-]+))*/i); + + //console.debug(matches); + + if ( matches == null ){ + return null; + } + + const lang = matches.at(1); + if ( lang === undefined ){ + return null; + } + + const params:string[] = matches.slice(2).filter( e => e ); + //console.debug({params}); + + return new CodeBlockInfo( lang, params ); + } + + // private matchesCodeBlockStart( line:string, languages:string[] ) : boolean { + + // const cbInfo = this.extractCodeBlockInfo(line); + + // if ( cbInfo == null ){ + // return false; + // } + + // return languages.contains( cbInfo?.language ); + // } + + public loadCsv( csvContent:string ) : DataSet { + const lines = csvContent.split('\n').map( e=>e.trim() ); + if (lines.length == 0){ + return new DataSet(); + } + + const columns = lines.first()?.split(',').map( e => this.extractAsColumnName(e) ) ?? []; + + const rows = new Array(); + + for (let i = 1; i < lines.length; i++) { + const rowData = lines[i].split(',').map( d => this.extractValueFromString(d)); + rows.push( new DataSetRow(columns, rowData)); + } + + const ds = new DataSet( ...rows ); + + return ds; + } + public applyMarkdownContent( name:string, content:string, data:IDataSetCollection, - templates:string[], - templateLanguages: string[] + templates:NamedCodeBlock[], + templateLanguageFilter: string[] ) : void { const lines = content.split('\n'); @@ -17,12 +139,15 @@ export class Parser { let currentHeader = ''; for ( let i = 0; i < lines.length; i++ ) { const trimLine = lines[i].trim(); - if (trimLine.startsWith('#')){ + + const codeBlockInfo = this.extractCodeBlockInfo(trimLine); + + if ( trimLine.startsWith('#') ) { // header - currentHeader = this.extractHeader(trimLine); + currentHeader = this.extractHeaderName(trimLine); - }else if ( trimLine.startsWith('|') ){ + } else if ( trimLine.startsWith('|') ){ // extract table const tableLines: string[] = []; @@ -31,10 +156,10 @@ export class Parser { tableLines.push(tableLine); i++; } - const dataPropName = this.convertToTableName( currentHeader.length > 0 ? currentHeader : name ); - data[dataPropName] = this.parseAsMdTable(tableLines); + const dataPropName = this.extractAsTableName( currentHeader.length > 0 ? currentHeader : name ); + data[dataPropName] = this.parseMdTable(tableLines); - }else if ( templateLanguages.find( v => trimLine.toLowerCase().startsWith('```' + v) ) !== undefined ){ + } else if ( codeBlockInfo && templateLanguageFilter.contains( codeBlockInfo.language ) ){ // extract template codeblock const templateLines: string[] = []; @@ -44,17 +169,24 @@ export class Parser { templateLines.push(templateLine); i++; } - templates.push( templateLines.join('\n') ); + + const codeBlock = new NamedCodeBlock( + codeBlockInfo, + name, + templateLines.join('\n') + ); + + templates.push( codeBlock ); } } } - private extractHeader(line:string) : string{ - return this.convertToTableName(line.replace('#','').trim()); + private extractHeaderName(line:string) : string{ + return line.replaceAll('#','').trim(); } - private parseAsMdTable( tableLines:string[] ): DataSet { + private parseMdTable( tableLines:string[] ): DataSet { const data: DataSet = new DataSet(); const tlines = tableLines @@ -63,7 +195,7 @@ export class Parser { //console.debug(tableLines); const columns = tlines .first()?.split('|') - .map( e=> this.convertToColumnName(e) ) + .map( e=> this.extractAsColumnName(e) ) .filter( e=>e.length > 0) ?? [] ; @@ -77,7 +209,7 @@ export class Parser { if (rowLine.startsWith('|') && rowLine.endsWith('|')){ continue; } - const rowValues : unknown[] = rowLine.split('|').map( e => this.covertFromString(e) ); + const rowValues : unknown[] = rowLine.split('|').map( e => this.extractValueFromString(e) ); data.push( new DataSetRow( columns, rowValues ) ); } @@ -113,9 +245,9 @@ export class Parser { const to = editor.offsetToPos(section.position.end.offset); const table = editor.getRange( from, to ); const tableLines = table.split('\n'); - const data = this.parseAsMdTable(tableLines); + const data = this.parseMdTable(tableLines); - const tableName = this.convertToTableName(lastHeading); + const tableName = this.extractAsTableName(lastHeading); result[tableName] = data; //result.push( data ); @@ -152,15 +284,15 @@ export class Parser { } - private convertToTableName( str:string ) : string { - return str.trim().replace(/\W/ig, '_').toLowerCase(); + private extractAsTableName( str:string ) : string { + return str.trim().replaceAll(/\W/ig, '_').toLowerCase(); } - private convertToColumnName( str:string ) : string { - return str.trim().replace(/\W/ig, '_').toLowerCase(); + private extractAsColumnName( str:string ) : string { + return str.trim().replaceAll(/\W/ig, '_').toLowerCase(); } - private covertFromString( str:string ) : string | number | Date { + private extractValueFromString( str:string ) : string | number | Date { const trimmed = str.trim(); //console.debug({trimmed}); //see: https://rgxdb.com/r/526K7G5W @@ -183,61 +315,5 @@ export class Parser { return trimmed; } - public fetchCodeBlocks( - editor: Editor, - fileCache: CachedMetadata | undefined, - languages :string[] - ): string[] { - const result: string[] = []; - - if ( fileCache == undefined ){ - return result; - } - - if ( fileCache.sections == undefined ){ - return result; - } - - fileCache.sections.forEach( section => { - if (section.type != 'code'){ - return; - } - const from = editor.offsetToPos(section.position.start.offset); - const to = editor.offsetToPos(section.position.end.offset); - const code = editor.getRange( from, to ); - let lines = code.split('\n'); - if (lines.length <= 2){ - return; - } - const lineStarts = languages.map( l=> '```'+l ); - if ( !lineStarts.find( e=> e.startsWith( lines[0].toLowerCase() ) )){ - return; - } - // remove first and last lines - lines = lines.slice(1,-1) - result.push( lines.join('\n') ); - }); - - return result; - } - public loadCsv( csvdata:string ) : DataSet { - const lines = csvdata.split('\n').map( e=>e.trim()); - if (lines.length == 0){ - return new DataSet(); - } - //console.debug(lines); - const columns = lines.first()?.split(',').map( e=> this.convertToColumnName(e) ) ?? []; - - const rows = new Array(); - - for (let i = 1; i < lines.length; i++) { - const rowData = lines[i].split(',').map( d=>this.covertFromString(d)); - rows.push( new DataSetRow(columns, rowData)); - } - - const ds = new DataSet( ...rows ); - //ds.columns = columns; - return ds; - } } diff --git a/src/run-context.ts b/src/run-context.ts index 1a2d932..d0ed3a2 100644 --- a/src/run-context.ts +++ b/src/run-context.ts @@ -1,17 +1,18 @@ import { DataSet, IDataSetCollection } from "src/data-set"; import { DataviewApi } from "obsidian-dataview"; import { RunLogger } from "src/run-logger"; +import { NamedCodeBlock } from "./named-code-block"; export type TRunContext = { sourceCode: string; data: IDataSetCollection; - templates: string[]; + templates: NamedCodeBlock[]; logger: RunLogger, log( ...params: any[] ) : Promise; - render( template:string, data:any ) : string; + render( template:string|NamedCodeBlock, data:any ) : string; ui: TUiRunContext; diff --git a/tsconfig.json b/tsconfig.json index 2d6fbdf..6fdb82a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "inlineSourceMap": true, "inlineSources": true, "module": "ESNext", - "target": "ES6", + "target": "ES2021", "allowJs": true, "noImplicitAny": true, "moduleResolution": "node", @@ -13,9 +13,10 @@ "strictNullChecks": true, "lib": [ "DOM", - "ES5", - "ES6", - "ES7" + //"ES5", + //"ES6", + //"ES7" + "ES2021" ] }, "include": [