diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..5e2b3f8 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +patreon: whitequark diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml new file mode 100644 index 0000000..2ba9ea2 --- /dev/null +++ b/.github/workflows/package.yml @@ -0,0 +1,87 @@ +on: [push, pull_request] +name: Test & publish +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Check out source code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up node + uses: actions/setup-node@v4 + with: + node-version: '18.x' + - name: Prepare metadata + run: node prepare.mjs + - name: Install dependencies + run: npm install + - name: Build generated code + run: npm run build + - name: Run tests + run: npm run test + - name: Build JavaScript artifact + run: | + mkdir -p dist + npm pack --pack-destination dist + - name: Upload JavaScript artifact + uses: actions/upload-artifact@v4 + with: + name: dist-npmjs + path: dist/ + - name: Build Python artifact + run: | + pip install build + python -m build pypi + - name: Upload Python artifact + uses: actions/upload-artifact@v4 + with: + name: dist-pypi + path: pypi/dist/ + check: # group all `test (*)` workflows into one for the required status check + needs: test + if: ${{ always() && !contains(needs.*.result, 'cancelled') }} + runs-on: ubuntu-latest + steps: + - run: ${{ contains(needs.*.result, 'failure') && 'false' || 'true' }} + publish-python: + needs: check + if: ${{ github.event_name == 'push' && github.repository == 'YoWASP/wavedrom' && github.event.ref == 'refs/heads/main' }} + runs-on: ubuntu-latest + environment: publish + permissions: + id-token: write + steps: + - name: Download Python artifacts + uses: actions/download-artifact@v4 + with: + name: dist-pypi + path: dist-tree/ + - name: Prepare artifacts for publishing + run: | + mkdir dist + find dist-tree -name '*.whl' -exec mv {} dist/ \; + - name: Publish wheels to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + publish-javascript: + needs: check + if: ${{ github.event_name == 'push' && github.repository == 'YoWASP/wavedrom' && github.event.ref == 'refs/heads/main' }} + runs-on: ubuntu-latest + environment: publish + permissions: + id-token: write + steps: + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + registry-url: 'https://registry.npmjs.org' + - name: Download JavaScript artifact + uses: actions/download-artifact@v4 + with: + name: dist-npmjs + path: dist/ + - name: Publish package to NPM + run: npm publish --access public $(find dist -name *.tgz -printf 'file:%p ') + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_CONFIG_PROVENANCE: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..640b896 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/package-lock.json +/package.json +/node_modules +/dist diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..91dc7e8 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Catherine + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2b4aa09 --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +YoWASP WaveDrom package +======================= + +This package provides a self-contained [WaveDrom] renderer for JavaScript and Python applications. See the [overview of the YoWASP project][yowasp] for details. + +[WaveDrom]: https://wavedrom.com/ +[yowasp]: https://yowasp.org/ + +Command-line tool +----------------- + +This package installs a command-line tool `yowasp-wavedrom`. + +``` +Usage: yowasp-wavedrom [] [] +``` + +API reference (JavaScript) +-------------------------- + +The [@yowasp/wavedrom] package has one entry point, `render(source): string`. It accepts a JavaScript object in the WaveJSON format and returns an SVG image serialized as a string: + +```js +import { render } from '@yowasp/wavedrom'; + +console.log(render({signal: [{ name: "clk", wave: "p..." }, { name: "data", wave: "01.0" }]})); +// => +``` + +[@yowasp/wavedrom]: https://www.npmjs.com/package/@yowasp/wavedrom + +API reference (Python) +---------------------- + +The [yowasp-wavedrom] package has one entry point, `render(source) -> str`. It accepts a Python dictionary in the WaveJSON format and returns an SVG image serialized as a string: + +```py +from yowasp_wavedrom import render + +print(render({"signal": [{ "name": "clk", "wave": "p..." }, { "name": "data", "wave": "01.0" }]})) +# => +``` + +[yowasp-wavedrom]: https://pypi.org/project/yowasp-wavedrom + +Implementation notes +-------------------- + +This package embeds the [upstream WaveDrom library][upstream] bundled with the minimal amount of dependencies necessary to produce a serialized SVG, and, for the Python package, with a JavaScript runtime. In addition, the output is post-processed compared to the upstream library as follows: + +* The `id` attribute of the root `` element is removed. +* The stylesheets are altered to take into account dark color scheme preference via media queries. + * When rendering waveform diagrams, the `default` skin automatically switches between light and dark color scheme, and the `light` skin corresponds to the upstream `default` skin. +* Several otherwise blocking bugs are worked around. + +[upstream]: https://npmjs.org/package/wavedrom + +Updates +------- + +Unlike most [YoWASP] packages, this package does not automatically track upstream releases. Please [open a pull request](https://github.com/YoWASP/wavedrom/pulls) bumping the version of `wavedrom` in `package-in.json` if you need a feature from a newer version of [WaveDrom]. + +License +------- + +This package is covered by the [MIT license](LICENSE.txt). \ No newline at end of file diff --git a/bin/yowasp-wavedrom.js b/bin/yowasp-wavedrom.js new file mode 100755 index 0000000..c4fcdd5 --- /dev/null +++ b/bin/yowasp-wavedrom.js @@ -0,0 +1,17 @@ +#!/usr/bin/env node + +import { readFileSync, writeFileSync } from 'fs'; +import { render } from '@yowasp/wavedrom'; + +const args = process.argv.slice(2); +if (!(args.length >= 0 && args.length <= 2)) { + console.error(`Usage: yowasp-wavedrom [- | ] [- | ]`); + process.exit(1); +} + +const inputPathOrFd = (args[0] === undefined || args[0] === '-') ? process.stdin.fd : args[0]; +const outputPathOrFd = (args[1] === undefined || args[1] === '-') ? process.stdout.fd : args[1]; + +const source = JSON.parse(readFileSync(inputPathOrFd, 'utf-8')); +const output = render(source); +writeFileSync(outputPathOrFd, output, 'utf-8'); diff --git a/build.mjs b/build.mjs new file mode 100644 index 0000000..10547fc --- /dev/null +++ b/build.mjs @@ -0,0 +1,42 @@ +import { build } from 'esbuild'; +import { componentize } from '@bytecodealliance/componentize-js'; +import { readFile, writeFile } from 'node:fs/promises'; + +// Bundle, for npm +await build({ + logLevel: 'info', + entryPoints: ['lib/api.js'], + bundle: true, + format: 'esm', + outfile: 'out/bundle.js', +}); + +// Componentize, for PyPI +// This isn't currently used pending https://github.com/bytecodealliance/wasmtime-py/pull/224, +// but is planned for later use and kept tested in the meantime. +const { component } = await componentize(`\ +${await readFile('out/bundle.js', 'utf8')} + +export function renderJson(sourceJSON) { + return render(JSON.parse(sourceJSON)); +} +`, `\ +package local:wavedrom; + +world wavedrom { + export render-json: func(json: string) -> string; +} +`, { + disableFeatures: ['random', 'stdio', 'clocks'] +}); +await writeFile('out/wavedrom.component.wasm', component); + +// Bundle, for PyPI +// This is what's actually used right now. +await build({ + logLevel: 'info', + entryPoints: ['lib/api.js'], + bundle: true, + format: 'cjs', + outfile: 'pypi/yowasp_wavedrom/bundle.js', +}); diff --git a/lib/api.d.ts b/lib/api.d.ts new file mode 100644 index 0000000..6c0e8cc --- /dev/null +++ b/lib/api.d.ts @@ -0,0 +1 @@ +export function render(source: any): string; \ No newline at end of file diff --git a/lib/api.js b/lib/api.js new file mode 100644 index 0000000..6978e8d --- /dev/null +++ b/lib/api.js @@ -0,0 +1,63 @@ +import { default as onml_stringify } from 'onml/stringify.js'; +import { default as WaveSkin_default } from 'wavedrom/skins/default.js'; +import { default as WaveSkin_narrow } from 'wavedrom/skins/narrow.js'; +import { default as WaveSkin_narrower } from 'wavedrom/skins/narrower.js'; +import { default as WaveSkin_narrowerer } from 'wavedrom/skins/narrowerer.js'; +import { default as WaveSkin_lowkey } from 'wavedrom/skins/lowkey.js'; +import { default as WaveSkin_dark } from 'wavedrom/skins/dark.js'; +import { default as WaveDrom_renderSignal } from 'wavedrom/lib/render-signal.js'; +import { default as WaveDrom_renderReg } from 'wavedrom/lib/render-reg.js'; +import { default as WaveDrom_renderAssign } from 'logidrom/lib/render-assign.js'; + +const WaveSkin = { + light: WaveSkin_default.default, + ...WaveSkin_dark, + ...WaveSkin_narrow, + ...WaveSkin_narrower, + ...WaveSkin_narrowerer, + ...WaveSkin_lowkey, +}; + +// Make the default skin respect browser dark mode. +WaveSkin.default = JSON.parse(JSON.stringify(WaveSkin.light)); +// `laneParamsFromSkin` uses hardcoded offsets into the skin to derive parameters, so the tag +// structure of a skin must be preserved exactly, e.g. there can be exactly one