This commit is contained in:
Cleon 2023-01-05 13:34:44 +13:00
parent 00db6be084
commit 7f1575a849
10 changed files with 248 additions and 102 deletions

View file

@ -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

View file

@ -35,7 +35,7 @@ esbuild.build({
...builtins],
format: 'cjs',
watch: !prod,
target: 'es2018',
target: 'es2021',
logLevel: "info",
sourcemap: prod ? false : 'inline',
treeShaking: true,

53
main.ts
View file

@ -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<string,string>();
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);
}
}

8
src/code-block-info.ts Normal file
View file

@ -0,0 +1,8 @@
export class CodeBlockInfo{
public language:string;
public params:string[];
constructor( language: string, params?:string[] ){
this.language = language;
this.params = params ?? [];
}
}

View file

@ -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<boolean>{

View file

@ -1,5 +1,5 @@
export class DataSet extends Array<DataSetRow> {
// TODO: is this needed?
}
export interface IDataSetCollection {

14
src/named-code-block.ts Normal file
View file

@ -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;
}
}

View file

@ -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<DataSetRow>();
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<DataSetRow>();
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;
}
}

View file

@ -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<void>;
render( template:string, data:any ) : string;
render( template:string|NamedCodeBlock, data:any ) : string;
ui: TUiRunContext;

View file

@ -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": [