diff --git a/src/app/component/plugin/plugin.component.html b/src/app/component/plugin/plugin.component.html index 6ffbce3b9..19a84c7ac 100644 --- a/src/app/component/plugin/plugin.component.html +++ b/src/app/component/plugin/plugin.component.html @@ -23,6 +23,10 @@ download + @if (plugin.config?.export) { + export + } delete diff --git a/src/app/component/plugin/plugin.component.ts b/src/app/component/plugin/plugin.component.ts index 851b51b2e..7c64d892c 100644 --- a/src/app/component/plugin/plugin.component.ts +++ b/src/app/component/plugin/plugin.component.ts @@ -9,8 +9,9 @@ import { Plugin, writePlugin } from '../../model/plugin'; import { tagDeleteNotice } from "../../mods/delete"; import { AdminService } from '../../service/admin.service'; import { PluginService } from '../../service/api/plugin.service'; +import { ModService } from '../../service/mod.service'; import { Store } from '../../store/store'; -import { downloadTag } from '../../util/download'; +import { downloadPluginExport, downloadTag } from '../../util/download'; import { scrollToFirstInvalid } from '../../util/form'; import { printError } from '../../util/http'; @@ -39,6 +40,7 @@ export class PluginComponent implements OnInit { schemaErrors: string[] = []; constructor( + private mod: ModService, public admin: AdminService, public store: Store, private plugins: PluginService, @@ -150,4 +152,8 @@ export class PluginComponent implements OnInit { download() { downloadTag(writePlugin(this.plugin)); } + + export() { + downloadPluginExport(this.plugin, this.mod.exportHtml(this.plugin)); + } } diff --git a/src/app/model/plugin.ts b/src/app/model/plugin.ts index cdb2b7bfc..d26538f1e 100644 --- a/src/app/model/plugin.ts +++ b/src/app/model/plugin.ts @@ -57,6 +57,10 @@ export interface Plugin extends Config { * Disable the editor and use the viewer to edit. */ editingViewer?: boolean; + /** + * This plugin can be exported to a self-contained html file. + */ + export?: boolean, /** * Show plugin as signature for existing tag. */ diff --git a/src/app/model/tag.ts b/src/app/model/tag.ts index bab53dc7a..828323b19 100644 --- a/src/app/model/tag.ts +++ b/src/app/model/tag.ts @@ -5,6 +5,7 @@ import { Schema } from 'jtd'; import { defer, isEqual, omitBy, uniqWith } from 'lodash-es'; import { toJS } from 'mobx'; import * as moment from 'moment'; +import { MomentInput } from 'moment/moment'; import { v4 as uuid } from 'uuid'; import { hasTag, prefix } from '../util/tag'; import { filterModels } from '../util/zip'; @@ -352,7 +353,7 @@ Handlebars.registerHelper('defer', (el: Element, fn: () => {}) => { } }); -Handlebars.registerHelper('fromNow', value => moment(value).fromNow()); +Handlebars.registerHelper('fromNow', (value: MomentInput) => moment(value).fromNow()); Handlebars.registerHelper('response', (ref: Ref, value: string) => { return ref.metadata?.userUrls?.includes(value); diff --git a/src/app/mods/ninga-triangle.ts b/src/app/mods/ninga-triangle.ts index f5536a18d..f036644cf 100644 --- a/src/app/mods/ninga-triangle.ts +++ b/src/app/mods/ninga-triangle.ts @@ -15,14 +15,15 @@ export const ninjaTrianglePlugin: Plugin = { submitText: true, add: true, genId: true, + export: true, generated: $localize`Generated by jasper-ui ${moment().toISOString()}`, description: $localize`Create a Japanese Triangle and show the longest Ninja Path.`, aiInstructions: ` # plugin/ninja.triangle - Let $n$ be a positive integer. A *Japanese triangle* consists of $1 + 2 + \\ldots + n$ circles arranged in an - equilateral triangular shape such that for each $i = 1, 2, \\ldots, n$, the $i$th row contains exactly $i$ circles, - exactly one of which is coloured red. A *ninja path* in a Japanese triangle is a sequence of $n$ circles obtained by - starting in the top row, then repeatedly going from a circle to one of the two circles immediately below it and - finishing in the bottom row. Here is an example of a Japanese triangle with $n = 6$. +Let $n$ be a positive integer. A *Japanese triangle* consists of $1 + 2 + \\ldots + n$ circles arranged in an +equilateral triangular shape such that for each $i = 1, 2, \\ldots, n$, the $i$th row contains exactly $i$ circles, +exactly one of which is coloured red. A *ninja path* in a Japanese triangle is a sequence of $n$ circles obtained by +starting in the top row, then repeatedly going from a circle to one of the two circles immediately below it and +finishing in the bottom row. Here is an example of a Japanese triangle with $n = 6$. r r o @@ -31,8 +32,8 @@ export const ninjaTrianglePlugin: Plugin = { o o o r o r o o o o o - In terms of $n$, find the greatest $k$ such that in each Japanese triangle there is a ninja path containing at - least $k$ red circles. +In terms of $n$, find the greatest $k$ such that in each Japanese triangle there is a ninja path containing at +least $k$ red circles. `, icons: [{ label: $localize`🔺️`, order: 2 }], filters: [ diff --git a/src/app/service/mod.service.ts b/src/app/service/mod.service.ts index 40341dbbd..f5c99657e 100644 --- a/src/app/service/mod.service.ts +++ b/src/app/service/mod.service.ts @@ -2,8 +2,10 @@ import { DOCUMENT } from '@angular/common'; import { Inject, Injectable } from '@angular/core'; import { Title } from '@angular/platform-browser'; import flatten from 'css-flatten'; +import { marked } from 'marked'; import { autorun, runInAction } from 'mobx'; import { of } from 'rxjs'; +import { Plugin } from '../model/plugin'; import { Store } from '../store/store'; import { AdminService } from './admin.service'; import { ConfigService } from './config.service'; @@ -92,6 +94,65 @@ export class ModService { this.titleService.setTitle(`${this.config.title} ± ${title}`); } + exportHtml(plugin: Plugin): string { + return ` + + + + ${plugin.name || plugin.tag} + + + + + + + ${plugin.config?.snippet || ''} + ${plugin.config?.css || ''} + + + + +

${plugin.name || plugin.tag}

+

${marked(plugin.config?.aiInstructions || '')}

+
+ + +`; + } + private getTheme(id: string, sources: Record[]) { if (!id) return []; return sources.filter(ts => ts[id]) diff --git a/src/app/util/download.ts b/src/app/util/download.ts index 097a080ad..7060c9127 100644 --- a/src/app/util/download.ts +++ b/src/app/util/download.ts @@ -2,7 +2,7 @@ import * as FileSaver from 'file-saver'; import * as JSZip from 'jszip'; import { Ext, writeExt } from '../model/ext'; import { Page } from '../model/page'; -import { writePlugin } from '../model/plugin'; +import { Plugin, writePlugin } from '../model/plugin'; import { Ref, writeRef } from '../model/ref'; import { Tag } from '../model/tag'; import { writeTemplate } from '../model/template'; @@ -46,3 +46,11 @@ export async function downloadSet(ref: Ref[], ext: Ext[], title: string) { return zip.generateAsync({ type: 'blob' }) .then(content => FileSaver.saveAs(content, title + '.zip')); } + +export function downloadPluginExport(plugin: Plugin, html: string) { + const title = plugin.name || plugin.tag.replace('/', '_'); + const zip = new JSZip(); + zip.file(title + '.html', html); + return zip.generateAsync({ type: 'blob' }) + .then(content => FileSaver.saveAs(content, title + '.zip')); +}