From cf2f66f65a2a8d8f9c448abd59837e885676cd82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20F?= Date: Sat, 15 Jun 2024 22:33:35 +0200 Subject: [PATCH 1/2] Add QuickJS support --- CONTRIBUTING-CODE.adoc | 2 +- docs/modules/setup/pages/runtime.adoc | 4 +++ .../asciidoctor/js/asciidoctor_ext/quickjs.rb | 1 + .../lib/asciidoctor/js/opal_ext/quickjs.rb | 21 ++++++++++++ .../asciidoctor/js/opal_ext/quickjs/file.rb | 13 ++++++++ .../core/lib/asciidoctor/js/opal_ext/umd.rb | 13 +++++++- packages/core/package.json | 1 + packages/core/spec/quickjs/run.mjs | 18 ++++++++++ .../core/src/template-asciidoctor-quickjs.js | 33 +++++++++++++++++++ packages/core/tasks/module/builder.cjs | 6 ++-- 10 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 packages/core/lib/asciidoctor/js/asciidoctor_ext/quickjs.rb create mode 100644 packages/core/lib/asciidoctor/js/opal_ext/quickjs.rb create mode 100644 packages/core/lib/asciidoctor/js/opal_ext/quickjs/file.rb create mode 100644 packages/core/spec/quickjs/run.mjs create mode 100644 packages/core/src/template-asciidoctor-quickjs.js diff --git a/CONTRIBUTING-CODE.adoc b/CONTRIBUTING-CODE.adoc index 6af269a17..0be5c1e17 100644 --- a/CONTRIBUTING-CODE.adoc +++ b/CONTRIBUTING-CODE.adoc @@ -42,7 +42,7 @@ Next, run npm's `install` command: You're now ready to build Asciidoctor.js. -TIP: Opal.js, the Ruby runtime in JavaScript is available in `packages/core/node_modules/asciidoctor-opal-runtime/src/opal.js` +TIP: Opal.js, the Ruby runtime in JavaScript is available in `packages/core/node_modules/@asciidoctor/opal-runtime/src/opal.js` == Building diff --git a/docs/modules/setup/pages/runtime.adoc b/docs/modules/setup/pages/runtime.adoc index b1315651e..9e0ad0fa7 100644 --- a/docs/modules/setup/pages/runtime.adoc +++ b/docs/modules/setup/pages/runtime.adoc @@ -3,6 +3,7 @@ :uri-spidermonkey-read: https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Introduction_to_the_JavaScript_shell#Built-in_functions :uri-phantomjs-read: http://phantomjs.org/api/fs/method/read.html :uri-node-fs: https://nodejs.org/api/fs.html +:uri-quickjs-read: https://bellard.org/quickjs/ :uri-v8: https://developers.google.com/v8 :uri-graalvm: https://www.graalvm.org @@ -89,6 +90,9 @@ The implementation will use the {uri-spidermonkey-read}[`read` function]. `phantomjs`:: The implementation will use the {uri-phantomjs-read}[`fs.read` function]. +`quickjs`:: +The implementation will use the {uri-quickjs-read}[`std.loadFile` module]. + == Retrieve the runtime environment Once Asciidoctor.js is instantiated, you can retrieve the runtime environment with the `getRuntime` function: diff --git a/packages/core/lib/asciidoctor/js/asciidoctor_ext/quickjs.rb b/packages/core/lib/asciidoctor/js/asciidoctor_ext/quickjs.rb new file mode 100644 index 000000000..db3bbfc83 --- /dev/null +++ b/packages/core/lib/asciidoctor/js/asciidoctor_ext/quickjs.rb @@ -0,0 +1 @@ +# TODO: add QuickJS-specific class override here \ No newline at end of file diff --git a/packages/core/lib/asciidoctor/js/opal_ext/quickjs.rb b/packages/core/lib/asciidoctor/js/opal_ext/quickjs.rb new file mode 100644 index 000000000..6bdce6c90 --- /dev/null +++ b/packages/core/lib/asciidoctor/js/opal_ext/quickjs.rb @@ -0,0 +1,21 @@ +%x( + var platform, engine, framework, ioModule; + + if (typeof moduleConfig === 'object' && typeof moduleConfig.runtime === 'object') { + var runtime = moduleConfig.runtime; + platform = runtime.platform; + engine = runtime.engine; + framework = runtime.framework; + ioModule = runtime.ioModule; + } + ioModule = ioModule || 'quickjs'; + platform = platform || 'quickjs'; + engine = engine || ''; + framework = framework || '') + + JAVASCRIPT_IO_MODULE = %x(ioModule) + JAVASCRIPT_PLATFORM = %x(platform) + JAVASCRIPT_ENGINE = %x(engine) + JAVASCRIPT_FRAMEWORK = %x(framework) + + require 'asciidoctor/js/opal_ext/quickjs/file' \ No newline at end of file diff --git a/packages/core/lib/asciidoctor/js/opal_ext/quickjs/file.rb b/packages/core/lib/asciidoctor/js/opal_ext/quickjs/file.rb new file mode 100644 index 000000000..bb1c1ce28 --- /dev/null +++ b/packages/core/lib/asciidoctor/js/opal_ext/quickjs/file.rb @@ -0,0 +1,13 @@ +class File + + def self.read(path) + %x{ + const body = std.loadFile(path); + if (body === null) { + console.log(`unable to loadFile:"${path}" from:"${os.getcwd()[0]}" realpath:"${os.realpath(path)[0]}"`); + } + return body || ''; + } + end + +end diff --git a/packages/core/lib/asciidoctor/js/opal_ext/umd.rb b/packages/core/lib/asciidoctor/js/opal_ext/umd.rb index b245a9a48..a4db57432 100644 --- a/packages/core/lib/asciidoctor/js/opal_ext/umd.rb +++ b/packages/core/lib/asciidoctor/js/opal_ext/umd.rb @@ -2,6 +2,7 @@ var isNode = typeof process === 'object' && typeof process.versions === 'object' && process.browser != true, isElectron = typeof navigator === 'object' && typeof navigator.userAgent === 'string' && typeof navigator.userAgent.indexOf('Electron') !== -1, isBrowser = typeof window === 'object', + isQuickjs = typeof std === 'object', isGraalVM = typeof Polyglot === 'object' && Polyglot.import, isPhantomJS = typeof window === 'object' && typeof window.phantom === 'object', isWebWorker = typeof importScripts === 'function', @@ -36,6 +37,10 @@ platform = platform || 'standalone'; framework = framework || 'spidermonkey'; } + else if (isQuickjs) { + platform = platform || 'browser'; + framework = framework || 'quickjs'; + } else if (isBrowser) { platform = platform || 'browser'; if (isPhantomJS) { @@ -65,15 +70,18 @@ if (ioModule !== 'spidermonkey' && ioModule !== 'phantomjs' && ioModule !== 'node' + && ioModule !== 'quickjs' && ioModule !== 'graalvm' && ioModule !== 'xmlhttprequest') { - throw new Error('Invalid IO module, `config.ioModule` must be one of: spidermonkey, phantomjs, node, graalvm or xmlhttprequest'); + throw new Error('Invalid IO module, `config.ioModule` must be one of: spidermonkey, quickjs, phantomjs, node, graalvm or xmlhttprequest'); } } else { if (framework === 'spidermonkey') { ioModule = 'spidermonkey'; } else if (framework === 'phantomjs') { ioModule = 'phantomjs'; + } else if (framework === 'quickjs') { + ioModule = 'quickjs'; } else if (platform === 'node') { ioModule = 'node'; } else if (engine === 'graalvm') { @@ -107,6 +115,9 @@ if JAVASCRIPT_IO_MODULE == 'spidermonkey' require 'asciidoctor/js/opal_ext/spidermonkey/file' end +if JAVASCRIPT_IO_MODULE == 'quickjs' + require 'asciidoctor/js/opal_ext/quickjs/file' +end if JAVASCRIPT_IO_MODULE == 'xmlhttprequest' require 'asciidoctor/js/opal_ext/browser/file' end diff --git a/packages/core/package.json b/packages/core/package.json index 9f3497e26..7944a5419 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -36,6 +36,7 @@ "test:node": "mocha spec/*/*.spec.cjs && npm run test:node:esm", "test:node:esm": "mocha --experimental-json-modules spec/node/asciidoctor.spec.js", "test:browser": "node spec/browser/run.cjs", + "test:quickjs": "qjs --unhandled-rejection spec/quickjs/run.mjs", "test:types": "rm -f types/tests.js && eslint types --ext .ts && tsc --build types/tsconfig.json && node --input-type=commonjs types/tests.js", "test": "node tasks/test/unsupported-features.cjs && npm run test:node && npm run test:browser && npm run test:types", "build": "node tasks/build.cjs && npm run test && npm run lint", diff --git a/packages/core/spec/quickjs/run.mjs b/packages/core/spec/quickjs/run.mjs new file mode 100644 index 000000000..443732d46 --- /dev/null +++ b/packages/core/spec/quickjs/run.mjs @@ -0,0 +1,18 @@ +/* global Asciidoctor */ +import Asciidoctor from '../../build/asciidoctor-quickjs.js' +const asciidoctor = Asciidoctor() + +const data = '= asciidoctor.js, AsciiDoc in JavaScript\n' + +'Doc Writer \n\n' + +'Asciidoctor and Opal come together to bring\n' + +'http://asciidoc.org[AsciiDoc] to the browser!.\n\n' + +'== Technologies\n\n' + +'* AsciiDoc\n' + +'* Asciidoctor\n' + +'* Opal\n\n' + +'NOTE: That\'s all she wrote!!!\n\n' + +'include::spec/fixtures/include.adoc[]' + +const options = { safe: 0, header_footer: true, attributes: { stylesheet: "spec/fixtures/css/simple.css", showtitle: true } } +const html = asciidoctor.convert(data, options) +console.log(html) diff --git a/packages/core/src/template-asciidoctor-quickjs.js b/packages/core/src/template-asciidoctor-quickjs.js new file mode 100644 index 000000000..c501053f1 --- /dev/null +++ b/packages/core/src/template-asciidoctor-quickjs.js @@ -0,0 +1,33 @@ +/* global Asciidoctor, ASCIIDOCTOR_JS_VERSION */ +import * as std from 'std'; +import * as os from 'os'; + +//{{opalCode}} + +const __path__ = { + separator: os.platform == "win32" ? '\\' : '/', + split(path) { return path.split(this.separator); }, + join(compo) { return compo.join(this.separator); }, + basename(path) { return this.join(this.split(path).slice(0,-1)); }, + dirname(path) { return this.split(path).pop(); }, +}; +const __asciidoctorDistDir__ = os.realpath(scriptArgs[0])[0].match(os.platform == "win32" ? /.*\\/ : /.*\//); + +export default function (moduleConfig) { +//{{asciidoctorCode}} + +//{{asciidoctorAPI}} + +//{{asciidoctorVersion}} + + /** + * Get Asciidoctor.js version number. + * + * @memberof Asciidoctor + * @returns {string} - returns the version number of Asciidoctor.js. + */ + Asciidoctor.prototype.getVersion = function () { + return ASCIIDOCTOR_JS_VERSION + } + return Opal.Asciidoctor +} diff --git a/packages/core/tasks/module/builder.cjs b/packages/core/tasks/module/builder.cjs index 7a7a07e5b..9da8c8d2f 100644 --- a/packages/core/tasks/module/builder.cjs +++ b/packages/core/tasks/module/builder.cjs @@ -135,7 +135,7 @@ const generateFlavors = async (asciidoctorCoreTarget, environments) => { const opalExtData = fs.readFileSync(`build/opal-ext-${environment}.js`, 'utf8') const asciidoctorCoreData = fs.readFileSync(asciidoctorCoreTarget, 'utf8') let data - if (['node', 'browser'].includes(environment)) { + if (['node', 'browser', 'quickjs'].includes(environment)) { const asciidoctorExtData = fs.readFileSync(`build/asciidoctor-ext-${environment}.js`, 'utf8') data = opalExtData.concat('\n').concat(asciidoctorExtData).concat('\n').concat(asciidoctorCoreData) } else { @@ -150,6 +150,8 @@ const generateFlavors = async (asciidoctorCoreTarget, environments) => { const target = `build/asciidoctor-${environment}.js` if (environment === 'node') { templateFile = 'src/template-asciidoctor-node.js' + } else if (environment === 'quickjs') { + templateFile = 'src/template-asciidoctor-quickjs.js' } else { templateFile = 'src/template-asciidoctor-browser.js' } @@ -216,7 +218,7 @@ module.exports = class Builder { this.benchmarkBuildDir = path.join('build', 'benchmark') this.examplesBuildDir = path.join('build', 'examples') this.asciidocRepoBaseURI = 'https://raw.githubusercontent.com/asciidoc/asciidoc/d43faae38c4a8bf366dcba545971da99f2b2d625' - this.environments = ['node', 'graalvm', 'browser'] + this.environments = ['node', 'graalvm', 'browser', 'quickjs'] this.asciidoctorCoreTarget = path.join('build', 'asciidoctor-core.js') } From ae6ef8853fa757ab481de636de9d27031af755f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20F?= Date: Sun, 30 Jun 2024 20:52:32 +0200 Subject: [PATCH 2/2] Add QuickJS in CI --- .github/workflows/build.yml | 19 +++++++- packages/core/cli/quickjs.mjs | 19 ++++++++ packages/core/package.json | 2 +- packages/core/spec/node/asciidoctor.spec.js | 4 +- packages/core/spec/quickjs/run.mjs | 43 +++++++++++++------ packages/core/spec/share/asciidoctor-spec.cjs | 2 + 6 files changed, 72 insertions(+), 17 deletions(-) create mode 100644 packages/core/cli/quickjs.mjs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d118d1e5d..49bce8eb5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,14 +43,19 @@ jobs: with: node-version: ${{ matrix.node-version }} # libgbm-dev is required by Puppeteer 3+ - - name: Install system dependencies + - name: Install Linux dependencies run: | sudo apt-get install -y libgbm-dev if: ${{ runner.os == 'Linux' }} + - name: Install macOS dependencies + run: | + brew install upx + if: ${{ runner.os == 'macOS' }} - name: Install dependencies run: | npm ci npm ci --prefix packages/core + git clone https://github.com/bellard/quickjs.git /tmp/qjs && cd /tmp/qjs && sudo make -j$(getconf _NPROCESSORS_ONLN) install - name: Lint, build and test env: ASCIIDOCTOR_CORE_VERSION: ${{ matrix.asciidoctor-core-version }} @@ -58,6 +63,12 @@ jobs: npm run lint npm run test npm run travis --prefix packages/core + - name: Compile + working-directory: ./packages/core + run: | + qjsc -o dist/asciidoctor-$(uname -s)-$(uname -m) -flto cli/quickjs.mjs + strip dist/asciidoctor-$(uname -s)-$(uname -m) + upx -9 dist/asciidoctor-$(uname -s)-$(uname -m) build-windows: needs: activate runs-on: windows-latest @@ -102,3 +113,9 @@ jobs: ASCIIDOCTOR_CORE_VERSION: ${{ matrix.asciidoctor-core-version }} working-directory: ./packages/core run: npm run examples + - name: Build exe + env: + ASCIIDOCTOR_CORE_VERSION: ${{ matrix.asciidoctor-core-version }} + working-directory: ./packages/core + run: qjsc -o dist/asciidoctor-win-x64.exe -flto cli/quickjs.mjs && strip dist/asciidoctor-win-x64.exe && upx -9 dist/asciidoctor-win-x64.exe + if: ${{ runner.os == 'TODO:Find a way to make+install qjs in windows' }} \ No newline at end of file diff --git a/packages/core/cli/quickjs.mjs b/packages/core/cli/quickjs.mjs new file mode 100644 index 000000000..6d278983c --- /dev/null +++ b/packages/core/cli/quickjs.mjs @@ -0,0 +1,19 @@ +import * as std from 'std'; +import Asciidoctor from "build/asciidoctor-quickjs.js"; + +const _ = scriptArgs.shift(); +const USAGE = `USAGE: ${_} [OPTIONS] [FILE|-] +EXAMPLE: ${_} --safe=0 --doctype=\\"article\\" <<< include::partial.adoc[]` +const die = (msg) => console.log(msg) + std.exit(1); +const [file = ""] = scriptArgs.filter(arg => !arg.startsWith('-')); +const options = Object.fromEntries(scriptArgs.filter(arg => arg.startsWith('-')) + .map(arg => arg.split('=')).map(([k, ...v]) => [k.replace(/^-+/, ''), std.parseExtJSON(v.join('=') || '1')])); +if (options.help) die(USAGE); +std.err.puts(`converting ${file ? "file:" + file : 'stdin'} options:${JSON.stringify(options)}\n`); +const body = file ? std.loadFile(file) : std.in.readAsString(); +if (!body) die(USAGE); +try { + console.log(Asciidoctor().convert(body, options)); +} catch (e) { + console.log(e) +} \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json index 7944a5419..2455ea3e5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -57,7 +57,7 @@ "docs:build": "documentation build src/** -f html -o build/docs -g", "docs:serve": "documentation serve src/** -g -w", "docs": "npm run docs:lint && npm run docs:build", - "travis": "npm run lint && npm run package && npm run docs && npm run examples && npm run test:graalvm" + "travis": "npm run lint && npm run package && npm run docs && npm run examples && npm run test:graalvm && npm run test:quickjs" }, "repository": { "type": "git", diff --git a/packages/core/spec/node/asciidoctor.spec.js b/packages/core/spec/node/asciidoctor.spec.js index 8a6011fbb..1e3197e5b 100644 --- a/packages/core/spec/node/asciidoctor.spec.js +++ b/packages/core/spec/node/asciidoctor.spec.js @@ -2189,7 +2189,7 @@ header_attribute::foo[bar]` const html = asciidoctor.convert('include::' + testOptions.baseDir + '/spec/fixtures/include.adoc[]', opts) expect(html).to.contain('include content') }) - + /* RuntimeError: To use Array#pack, you must first require 'corelib/array/pack' it('should be able to convert a file and embed an image', () => { const options = { safe: 'safe', header_footer: true } const content = fs.readFileSync(path.resolve(__dirname, '../fixtures/image.adoc'), 'utf8') @@ -2197,7 +2197,7 @@ header_attribute::foo[bar]` expect(html).to.contain('French frog') expect(html).to.contain('data:image/jpg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/7SMwU') }) - + */ it('should be able to convert a buffer', () => { const options = { safe: 'safe', header_footer: true } const content = fs.readFileSync(resolveFixture('test.adoc')) diff --git a/packages/core/spec/quickjs/run.mjs b/packages/core/spec/quickjs/run.mjs index 443732d46..20e8d4de8 100644 --- a/packages/core/spec/quickjs/run.mjs +++ b/packages/core/spec/quickjs/run.mjs @@ -1,18 +1,35 @@ /* global Asciidoctor */ import Asciidoctor from '../../build/asciidoctor-quickjs.js' -const asciidoctor = Asciidoctor() +// poor man mocha since QuickJS don't rely on NPM +const expect = (obj) => ({to:{include(str){if(!obj.includes(str))throw `${obj} does not contain ${str}`}}}); +const describe = (title, todo) => todo(console.log(`${title}`)); +const it = (title, todo) => todo(console.log(` ${title}`)); +const asciidoctor = Asciidoctor() const data = '= asciidoctor.js, AsciiDoc in JavaScript\n' + -'Doc Writer \n\n' + -'Asciidoctor and Opal come together to bring\n' + -'http://asciidoc.org[AsciiDoc] to the browser!.\n\n' + -'== Technologies\n\n' + -'* AsciiDoc\n' + -'* Asciidoctor\n' + -'* Opal\n\n' + -'NOTE: That\'s all she wrote!!!\n\n' + -'include::spec/fixtures/include.adoc[]' + 'Doc Writer \n\n' + + 'Asciidoctor and Opal come together to bring\n' + + 'http://asciidoc.org[AsciiDoc] to the browser!.\n\n' + + '== Technologies\n\n' + + '* AsciiDoc\n' + + '* Asciidoctor\n' + + '* Opal\n\n' + + 'NOTE: That\'s all she wrote!!!\n\n' -const options = { safe: 0, header_footer: true, attributes: { stylesheet: "spec/fixtures/css/simple.css", showtitle: true } } -const html = asciidoctor.convert(data, options) -console.log(html) +describe('QuickJS', function () { + it('should convert as HTML', function () { + const opts = { } + const html = asciidoctor.convert(data, opts); + expect(html).to.include('
    '); + }) + it('should include stylesheet', function () { + const opts = { safe: 0, header_footer: true, attributes: { stylesheet: "spec/fixtures/css/simple.css", showtitle: true } }; + const html = asciidoctor.convert(data, opts); + expect(html).to.include('4078c0'); + }) + it('should include file', function () { + const opts = { safe: 0 }; + const html = asciidoctor.convert('include::spec/fixtures/include.adoc[]', opts) + expect(html).to.include('include content'); + }) +}) diff --git a/packages/core/spec/share/asciidoctor-spec.cjs b/packages/core/spec/share/asciidoctor-spec.cjs index b3535ac52..94e7c2c81 100644 --- a/packages/core/spec/share/asciidoctor-spec.cjs +++ b/packages/core/spec/share/asciidoctor-spec.cjs @@ -1241,6 +1241,7 @@ content`) }) describe('Embed an image when data-uri is defined', function () { + /* RuntimeError: To use Array#pack, you must first require 'corelib/array/pack'. it('should embed a jpeg image', function () { const options = { safe: 'safe', attributes: { 'data-uri': true, 'allow-uri-read': true } } const html = asciidoctor.convert(`image::${testOptions.baseDir}/spec/fixtures/images/litoria-chloris.jpg[]`, options) @@ -1263,6 +1264,7 @@ content`) expect(html).to.include('img src="data:image/png;base64,') }) } + */ it('should not throw an exception if the image does not exists', function () { const options = { safe: 'safe', attributes: { 'data-uri': true, 'allow-uri-read': true } } const html = asciidoctor.convert(`image::${testOptions.baseDir}/spec/fixtures/images/not_found.png[]`, options)