UNPKG

nadesiko3

Version:
255 lines (237 loc) 10.1 kB
// wnako3mod - nadesiko for web browser // - wnako3 から wnako3mod を取り込む。 // - このファイルはモジュールとして別のファイルから取り込み可能。 import { NakoCompiler, LoaderTool, LoaderToolTask } from '../core/src/nako3.mjs' import { NakoGlobal } from '../core/src/nako_global.mjs' import { NakoImportError } from '../core/src/nako_errors.mjs' import { Token } from '../core/src/nako_types.mjs' import { setupEditor } from './wnako3_editor.mjs' import nakoVersion, { NakoVersion } from './nako_version.mjs' import PluginBrowser from './plugin_browser.mjs' const NAKO_SCRIPT_RE = /^(なでしこ|nako|nadesiko)3?$/ export class WebNakoCompiler extends NakoCompiler { version: string wnakoVersion: NakoVersion localFiles: Record<string, string> constructor() { super({ useBasicPlugin: true }) this.version = nakoVersion.version this.wnakoVersion = nakoVersion this.localFiles = {} // プラグインを追加 this.addPluginObject('PluginBrowser', PluginBrowser) // 必要な定数を設定 this.addListener('beforeRun', (g: NakoGlobal) => { // バージョン情報を設定 g.__varslist[0].set('ナデシコ種類', 'wnako3') g.__varslist[0].set('ナデシコバージョン', nakoVersion.version) g.__varslist[0].set('WNAKOバージョン', nakoVersion.version) }) } /** * ブラウザでtype="なでしこ"というスクリプトを得て実行する */ async runNakoScript() { // スクリプトタグの中身を得る let nakoScriptCount = 0 const scripts = document.querySelectorAll('script') for (let i = 0; i < scripts.length; i++) { const script = scripts[i] if (script.type.match(NAKO_SCRIPT_RE)) { nakoScriptCount++ // URLからスクリプト名を見つける const url = (typeof (window.location) === 'object') ? window.location.href : 'url_unknown' const fname = `${url}#script${nakoScriptCount}.nako3` const code = script.text // 依存するライブラリをロード await this.loadDependencies(code, fname) // プログラムを実行 await this.runAsync(script.text, fname) } } if (nakoScriptCount > 1) { console.log('実行したなでしこの個数=', nakoScriptCount) } } /** 取り込む文を実行する */ async loadDependencies(code: string, filename: string, preCode = '', localFiles: any = {}): Promise<unknown> { this.localFiles = localFiles || {} return this._loadDependencies(code, filename, preCode, this.getLoaderTool()) } /** * type=なでしこ のスクリプトを自動実行するべきかどうかを返す * @returns type=なでしこ のスクリプトを自動実行するべきかどうか */ checkScriptTagParam(): boolean { const scripts = document.querySelectorAll('script') for (let i = 0; i < scripts.length; i++) { const script = scripts[i] const src = script.src || '' if (src.indexOf('wnako3.js?run') >= 0 || src.indexOf('wnako3.js&run') >= 0) { return true } } return false } /** * 指定したidのHTML要素をなでしこ言語のエディタにする。 * @param {string | Element} idOrElement HTML要素 * @see {setupEditor} */ setupEditor(idOrElement: any) { return setupEditor(idOrElement, this, (window as any).ace) } /** なでしこ3の『取り込む』命令のための読み込みツール */ getLoaderTool(): LoaderTool { const tool = { readJs: this.readJs, readNako3: this.readNako3, resolvePath: this.resolvePath } return tool } /** JSプラグインの読み込み */ readJs(filePath: string, token: Token): LoaderToolTask<any> { if (this.localFiles && this.localFiles[filePath]) { return { task: (async() => () => { Function(this.localFiles[filePath])() return {} })() } } return { task: (async() => { // もし動的インポートに対応していれば動的インポートを試す const basename = ('/' + filePath).split('/').pop() || '?' /* try { const obj = await import(filePath) this.addPluginObject(basename, obj) return {} } catch (e) { } */ // WebPackの関係で動的インポートが失敗するので、テキストとしてJSソースを取り出して処理する // JavaScriptをテキストとして取得 const res = await fetch(filePath) if (!res.ok) { throw new NakoImportError(`ファイル『${filePath}』のダウンロードに失敗しました: ${res.status} ${res.statusText}`, token.file, token.line) } let jstext = await res.text() // ESModuleっぽければ `default export`を置き換える const r: number = Math.floor(Math.random() * 1000000) const exportName = `exportObject${r}` if (jstext.includes('default export')) { jstext = jstext.replace('default export', `;let ${exportName} = `) jstext += `\n;navigator.nako3.addPluginObject('${basename}', ${exportName});\n` } // addPluginObjectが存在する if (!jstext.includes('navigator.nako3.addPluginObject')) { throw new NakoImportError(`ファイル ${filePath} の中に文字列 "navigator.nako3.addPluginObject" が存在しません。現在、ブラウザ版のなでしこ言語v3は自動登録するプラグインのみをサポートしています。`, token.file, token.line) } const gNavigator: any = navigator const gSelf: any = gNavigator.nako3 // textの例: `navigator.nako3.addPluginObject('PluginRequireTest', { requiretest: { type: 'var', value: 100 } })` return () => { // プラグインの自動登録は navigator.nako3 を参照するため、 navigator.nako3 を一時的に現在のインスタンスにする。 const globalNavigator: any = gNavigator const globalNako3 = globalNavigator.nako3 globalNavigator.nako3 = gSelf try { const f = Function(jstext) f.apply(gSelf.__globalObj) } catch (err) { throw new NakoImportError(`プラグイン ${filePath} の取り込みに失敗: ${err instanceof Error ? err.message : err + ''}`, token.file, token.line) } finally { globalNavigator.nako3 = globalNako3 } return {} } })() } } /** NAKO3プラグインの読み込み */ readNako3(filePath: string, token: Token): LoaderToolTask<string> { if (this.localFiles && this.localFiles[filePath]) { // ローカルファイルを使う場合 return { task: (() => { return (new Promise((resolve, reject) => { const s: string = this.localFiles[filePath] resolve(s) })) })() } } return { task: (async() => { const res = await fetch(filePath) if (!res.ok) { throw new NakoImportError(`ファイル ${filePath} のダウンロードに失敗しました: ${res.status} ${res.statusText}`, token.file, token.line) } return await res.text() })() } } /** 読み込みでパスを解決する */ resolvePath(name: string, token: Token, fromFile: string) { let pathname = name // http から始まっていれば解決は不要 if (pathname.match(/^https?:\/\//)) { // フルパスなら解決不要 } else if (this.localFiles && this.localFiles[name]) { pathname = this.localFiles[name] } else { try { pathname = new URL(name).pathname } catch (e) { // 単純にパスに変換できなければ、location.hrefを参考にパスを組み立てる try { let baseDir = dirname(fromFile) if (baseDir === '') { // https://2/3/4.html const a = window.location.href.split('/') baseDir = '/' + a.slice(3, a.length - 1).join('/') } pathname = resolveURL(baseDir, name) } catch (e) { throw new NakoImportError(`取り込み文の引数でパスが解決できません。https:// か http:// で始まるアドレスを指定してください。\n${e as string}`, token.file, token.line) } } } // 拡張子によってプラグインタイプを分岐する const checkExt = (name: string, extlist: string[]): boolean => { for (const ext of extlist) { if (name.endsWith(ext)) { return true } if (name.endsWith(ext + '.txt')) { return true } // '.mjs.txt' にもマッチ } return false } // URLパラメータを考慮する const aname: string = (pathname + '?').split('?')[0] // JSプラグイン → (.js|.mjs) // NAKO3プラグイン → (.nako3|.nako) if (checkExt(aname, ['.js', '.mjs'])) { return { filePath: pathname, type: 'js' } } if (checkExt(aname, ['.nako3', '.nako'])) { return { filePath: pathname, type: 'nako3' } } if (checkExt(pathname, ['.js', '.mjs'])) { return { filePath: pathname, type: 'js' } } if (checkExt(pathname, ['.nako3', '.nako'])) { return { filePath: pathname, type: 'nako3' } } // ファイル拡張子が未指定の場合 throw new NakoImportError(`ファイル『${name}』は拡張子が(.nako3|.js|.js.txt|.mjs|.mjs.txt)以外なので取り込めません。`, token.file, token.line) } } function dirname(s: string) { const a = s.split('/') if (a && a.length > 1) { return a.slice(0, a.length - 1).join('/') } return '' } function resolveURL(base: string, s: string) { const baseA = base.split('/') const sA = s.split('/') for (const p of sA) { if (p === '') { continue } if (p === '.') { continue } if (p === '..') { baseA.pop() continue } baseA.push(p) } return baseA.join('/') }