namespace OS { declare var JSZip: any; declare var $:any; export namespace application { export type AnteditBaseExtension = typeof EditorBaseExtension; } /** * * * @class EditorBaseExtension */ class EditorBaseExtension { static dependencies: string[]; protected app: OS.application.Antedit; protected name: string; constructor(name:string, app: OS.application.Antedit) { this.app = app; this.name = name; } /** * * * @returns {Promise} * @memberof EditorBaseExtension */ preload(): Promise { return API.require(OS.application.Antedit.extensions[this.name].dependencies); } /** * * * @protected * @returns {string} * @memberof EditorBaseExtension */ protected basedir(): string { return `${this.app.meta().path}/extensions/${this.name}`; } /** * * * @protected * @param {(string | FormattedString)} m * @returns {void} * @memberof EditorBaseExtension */ protected notify(m: string | FormattedString): void { return this.app.notify(m); } /** * * * @protected * @param {(string | FormattedString)} m * @param {Error} e * @returns {void} * @memberof EditorBaseExtension */ protected error(m: string | FormattedString, e: Error): void { return this.app.error(m, e); } /** * * * @protected * @return {AnteditLogger} editor logger * @memberof EditorBaseExtension */ protected logger(): any { if (!this.app.setting.showBottomBar) { this.app.showOutput(true); } else { this.app.showOutput(false); } return this.app.logger; } /** * * * @protected * @param {string} file * @returns {Promise>} * @memberof EditorBaseExtension */ protected metadata(file: string): Promise> { return new Promise((resolve, reject) => { if (!this.app.currdir) { return reject( API.throwe(__("Current folder is not found")) ); } `${this.app.currdir.path}/${file}` .asFileHandle() .read("json") .then((data) => { if (!data.root && this.app.currdir) { data.root = this.app.currdir.path; } resolve(data); }) .catch((e) => { // try to ask user to select a folder this.app.openDialog("FileDialog", { title: __("Select build directory"), root: this.app.currdir.path, mimes: ["dir"] }) .then((d) => { `${d.file.path}/${file}` .asFileHandle() .read("json") .then((data) => { if (!data.root) { data.root = d.file.path; } resolve(data); }) .catch((e1) => reject(e1)) }) .catch( (e1) => reject(API.throwe(__("Unable to read meta-data")) )) }); }); } } EditorBaseExtension.dependencies = []; OS.application.Antedit.extensions = {}; OS.application.Antedit.EditorBaseExtension = EditorBaseExtension; class EditorExtensionMaker extends EditorBaseExtension { constructor(app: OS.application.Antedit) { super("EditorExtensionMaker", app); } create(): void { this.logger().clear(); this.app .openDialog("FileDialog", { title: "__(New extension at)", file: { basename: __("ExtensionName") }, mimes: ["dir"], }) .then((d) => { return this.mktpl(d.file.path, d.name); }); } build(callback?: () => void): void { this.logger().clear(); this.metadata("extension.json") .then(async (meta) => { try { const jsrc = await API.VFS.cat(meta.javascripts.map(v => `${meta.root}/${v}`),""); await `${meta.root}/build/debug/main.js` .asFileHandle() .setCache(jsrc) .write("text/plain"); await `${meta.root}/build/debug/extension.json` .asFileHandle() .setCache(meta.meta) .write("object"); await API.VFS.copy( meta.copies.map(v => `${meta.root}/${v}`),`${meta.root}/build/debug`); this.logger().info(__("Files generated in {0}", `${meta.root}/build/debug`)); if(callback) callback(); } catch (e) { return this.logger().error(__("Unable to build extension:{0}", e.stack)); } }) .catch((e) => this.logger().error(__("Unable to read meta-data:{0}", e.stack))); } run(): void { this.logger().clear(); this.metadata("extension.json") .then(async (meta) => { if(!meta || !meta.meta || !meta.meta.name) return this.logger().error(__("Invalid extension meta-data")); try { const path = `${meta.root}/build/debug/main.js`; if (API.shared[path]) { delete API.shared[path]; } await API.requires(path); if (this.app.extensions[meta.meta.name] && this.app.extensions[meta.meta.name].cleanup) { this.app.extensions[meta.meta.name].cleanup(); } this.app.extensions[meta.meta.name] = new OS.application.Antedit.extensions[meta.meta.name](this.app); for (let v of meta.meta.actions) { this.app.eum.addAction(meta.meta, v, (e_name, a_name) => { this.app.loadAndRunExtensionAction(e_name, a_name, `${meta.root}/build`); }); } this.app.eum.active.getEditor().trigger(meta.meta.name, 'editor.action.quickCommand'); } catch (e) { return this.logger().error(__("Unable to run extension:{0}", e.stack)); } }) .catch((e) => this.logger().error(__("Unable to read meta-data:{0}", e.stack))); } release(): void { this.logger().clear(); this.metadata("extension.json") .then(async (meta) => { this.build(async () => { try { await API.VFS.mkar( `${meta.root}/build/debug`, `${meta.root}/build/release/${meta.meta.name}.zip` ); this.logger().info(__("Archive created at {0}", `${meta.root}/build/release/${meta.meta.name}.zip`)); } catch (e) { return this.logger().error( __("Unable to create archive: {0}", e.stack )); } }); }) .catch((e) => this.logger().error(__("Unable to read meta-data: {0}", e.stack))); } install(): void { this.logger().clear(); this.app .openDialog("FileDialog", { title: "__(Select extension archive)", mimes: [".*/zip"], }) .then(async (d) => { try { await this.installZip(d.file.path); this.logger().info(__("Extension installed")); return this.app.loadExtensionMetaData(); } catch (e) { return this.logger().error(__("Unable to install extension: {0}", e.stack)); } }); } installFromURL(): void { this.logger().clear(); this.app .openDialog("PromptDialog", { title: __("Enter URI"), label: __("Please enter extension URI:") }) .then(async (v) => { if(!v) return; try { await this.installZip(v); this.logger().info(__("Extension installed")); return this.app.loadExtensionMetaData(); } catch (e) { return this.app.error(__("Unable to install extension: {0}", v)); } }); } /** * * * @private * @param {string} path * @param {string} name * @memberof EditorExtensionMaker */ private mktpl(path: string, name: string): void { const rpath = `${path}/${name}`; const dirs = [ rpath, `${rpath}/build`, `${rpath}/build/release`, `${rpath}/build/debug`, ]; const files = [ ["main.tpl", `${rpath}/${name}.js`], ["meta.tpl", `${rpath}/extension.json`], ]; API.VFS.mkdirAll(dirs, true) .then(async () => { try { await API.VFS.mktpl(files, this.basedir(), (data)=>{ return data.format(name, `${path}/${name}`); }); this.app.currdir = rpath.asFileHandle(); this.app.toggleSideBar(); return this.app.eum.active.openFile( `${rpath}/${name}.js`.asFileHandle() as OS.application.EditorFileHandle ); } catch (e) { return this.logger().error( __("Unable to create extension template: {0}", e.stack) ); } }) .catch((e) => this.logger().error(__("Unable to create extension directories: {0}", e.stack)) ); } /** * * * @param {string} name extension name * @returns {Promise} * @memberof EditorExtensionMaker */ uninstall(name: string): Promise { return new Promise(async (resolve, reject) => { try { const ext_path = `${this.app.meta().path}/extensions`; const fp = `${ext_path}/extensions.json`.asFileHandle(); const meta = await fp.read("json"); let ext_meta = undefined; let ext_index = undefined; for(let idx in meta) { if(meta[idx].name === name) { ext_meta = meta[idx]; ext_index = idx; break; } } if(ext_meta === undefined) { return resolve(); } // remove the directory await `${ext_path}/${name}`.asFileHandle().remove(); // update the extension file meta.splice(ext_index, 1); fp.cache = meta; await fp.write('object'); resolve(); } catch(e) { reject(e); } }); } /** * * * @param {string} path * @returns {Promise} * @memberof EditorExtensionMaker */ installZip(path: string): Promise { return new Promise(async (resolve, reject) => { try{ await API.requires("os://scripts/jszip.min.js"); const data = await path.asFileHandle().read("binary"); const zip = await JSZip.loadAsync(data); const d = await zip.file("extension.json").async("uint8array"); const meta = JSON.parse(new TextDecoder("utf-8").decode(d)); // uninstall if exists await this.uninstall(meta.name); const pth = this.ext_dir(meta.name); const dir = [pth]; const files = []; for (let name in zip.files) { const file = zip.files[name]; if (file.dir) { dir.push(pth + "/" + name); } else if(name != "extension.json") { files.push(name); } } if (dir.length > 0) { await API.VFS.mkdirAll(dir, true) await this.installFiles(files, zip, meta); } else { await this.installFiles(files, zip, meta); } resolve(); } catch(e) { reject(__e(e)); } }); } private ext_dir(en: string): string { return `${this.app.meta().path}/extensions/${en}`; } /** * * * @private * @param {string[]} files * @param {*} zip * @param {GenericObject} meta * @returns {Promise} * @memberof EditorExtensionMaker */ private installFiles( files: string[], zip: any, meta: GenericObject ): Promise { if (files.length === 0) { return this.installMeta(meta); } return new Promise(async (resolve, reject) => { try{ const file = files.splice(0, 1)[0]; const path = `${this.ext_dir(meta.name)}/${file}`; const d = await zip.file(file).async("uint8array"); const r = await path.asFileHandle() .setCache(new Blob([d], { type: "octet/stream" })) .write("text/plain"); if (r.error) { return reject(r.error); } await this.installFiles(files, zip, meta); resolve(); } catch(e) { reject(__e(e)); } }); } /** * * * @private * @param {GenericObject} meta * @returns {Promise} * @memberof EditorExtensionMaker */ private installMeta(meta: GenericObject): Promise { return new Promise(async (resolve, reject) => { const file = `${this.ext_dir("")}/extensions.json`.asFileHandle(); try { const data = await file.read("json"); const names = []; for (let v of data) { names.push(v.name); } const idx = names.indexOf(meta.name); if (idx >= 0) { data.splice(idx, 1); } data.push(meta); try { await file.setCache(data).write("object"); return resolve(); } catch (e) { return reject(__e(e)); } } catch (e_1) { // try to create new file try { await file.setCache([meta]).write("object"); return resolve(); } catch (e_2) { return reject(__e(e_2)); } } }); } } OS.application.Antedit.extensions.EditorExtensionMaker = EditorExtensionMaker; }