From f0755afce5ae61265f4891d9588a3de438a3c878 Mon Sep 17 00:00:00 2001 From: SilenceLurker <78900386+SilenceLurker@users.noreply.github.com> Date: Thu, 5 Feb 2026 17:19:40 +0800 Subject: [PATCH 1/2] Update CodeQL workflow branches and schedule --- .github/workflows/codeql.yml | 99 ++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..8a3196a --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,99 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "main", "dev", "SL-Dev-2026" ] + pull_request: + branches: [ "main", "dev", "SL-Dev-2026" ] + schedule: + - cron: '0 18 */3 * *' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: javascript-typescript + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - name: Run manual build steps + if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" From 0d6f26c8d97ba4f6ae0d8df5818a1f3a314e4a11 Mon Sep 17 00:00:00 2001 From: Silence_Lurker Date: Fri, 13 Feb 2026 13:04:39 +0800 Subject: [PATCH 2/2] 2602 dev init --- SL/module/Module.js | 351 +++++++++++++++++++++++++++++++++++++++ SL/module/TableModule.js | 22 +++ 2 files changed, 373 insertions(+) create mode 100644 SL/module/Module.js create mode 100644 SL/module/TableModule.js diff --git a/SL/module/Module.js b/SL/module/Module.js new file mode 100644 index 0000000..a57379e --- /dev/null +++ b/SL/module/Module.js @@ -0,0 +1,351 @@ +export class Module { + constructor(builder) { + if (!builder || typeof builder.build !== 'function') { + throw new Error('[Module] Invalid builder.'); + } + this.builder = builder; + this.config = builder.build(); + this.ctx = null; + this.el = null; + this.viewEl = null; + this._bindings = []; + this._disposables = []; + this._state = { ...(this.config.state || {}) }; + } + + async init(ctx = {}) { + this.ctx = ctx; + await this._loadView(); + this._bindAll(); + return this; + } + + async mount() { + if (this._isStrict('mount')) { + this._abstract('mount'); + } + } + + dispose() { + if (this._isStrict('dispose')) { + this._abstract('dispose'); + } + this._unbindAll(); + for (const d of this._disposables) { + try { d(); } catch (_) { /* noop */ } + } + this._disposables = []; + } + + expose() { + return {}; + } + + getState() { + return { ...this._state }; + } + + setState(next) { + if (!next || typeof next !== 'object') return; + Object.assign(this._state, next); + this._applyStateToBindings(); + } + + registerDisposable(fn) { + if (typeof fn === 'function') { + this._disposables.push(fn); + } + } + + _abstract(methodName) { + throw new Error(`[Module] Method not implemented: ${methodName}`); + } + + _isStrict(methodName) { + if (!this.config.strict) return false; + const required = this.config.requiredMethods || []; + return required.includes(methodName); + } + + async _loadView() { + const viewPath = this.config.view; + if (!viewPath) return; + + const rootTarget = this._resolveRoot(); + if (!rootTarget) { + throw new Error('[Module] Root element not found.'); + } + + const url = this._resolveViewUrl(viewPath); + const res = await fetch(url); + if (!res.ok) { + throw new Error(`[Module] Failed to load view: ${viewPath}`); + } + const html = await res.text(); + + const wrapper = document.createElement('div'); + wrapper.className = this.config.wrapperClass || 'amily2-module'; + wrapper.dataset.module = this.config.name || 'Module'; + wrapper.innerHTML = html; + + rootTarget.appendChild(wrapper); + this.el = wrapper; + this.viewEl = wrapper; + } + + _resolveRoot() { + if (this.config.rootSelector) { + return document.querySelector(this.config.rootSelector); + } + if (this.ctx && this.ctx.root instanceof HTMLElement) { + return this.ctx.root; + } + return document.body; + } + + _resolveViewUrl(viewPath) { + if (/^(https?:)?\/\//.test(viewPath) || viewPath.startsWith('/')) { + return viewPath; + } + if (this.ctx && this.ctx.baseUrl) { + const baseUrl = this.ctx.baseUrl; + const absoluteBase = /^(https?:)?\/\//.test(baseUrl) + ? baseUrl + : `${window.location.origin}/${String(baseUrl).replace(/^\/+/, '')}`; + return new URL(viewPath, absoluteBase).toString(); + } + return new URL(viewPath, import.meta.url).toString(); + } + + _bindAll() { + this._bindVars(); + this._bindEvents(); + } + + _bindVars() { + const bindings = this._normalizeBindings(this.config.bindVars); + for (const [selector, spec] of Object.entries(bindings)) { + const el = this._query(selector); + if (!el) continue; + + const normalized = this._normalizeVarSpec(spec); + const { key, attr, event, parser, formatter } = normalized; + + const applyValue = () => { + const value = formatter ? formatter(this._state[key]) : this._state[key]; + if (attr === 'checked') { + el.checked = !!value; + } else if (attr in el) { + el[attr] = value ?? ''; + } else { + el.setAttribute(attr, value ?? ''); + } + }; + + const onInput = (e) => { + let value; + if (attr === 'checked') { + value = e.target.checked; + } else if (attr in e.target) { + value = e.target[attr]; + } else { + value = e.target.getAttribute(attr); + } + this._state[key] = parser ? parser(value) : value; + }; + + applyValue(); + el.addEventListener(event, onInput); + this._bindings.push(() => el.removeEventListener(event, onInput)); + } + } + + _bindEvents() { + const bindings = this._normalizeBindings(this.config.bindEvents); + for (const [selector, events] of Object.entries(bindings)) { + const el = this._query(selector); + if (!el) continue; + + for (const [eventName, handler] of Object.entries(events)) { + const fn = typeof handler === 'function' + ? handler.bind(this) + : (this[handler] ? this[handler].bind(this) : null); + if (!fn) continue; + el.addEventListener(eventName, fn); + this._bindings.push(() => el.removeEventListener(eventName, fn)); + } + } + } + + _applyStateToBindings() { + const bindings = this._normalizeBindings(this.config.bindVars); + for (const [selector, spec] of Object.entries(bindings)) { + const el = this._query(selector); + if (!el) continue; + const normalized = this._normalizeVarSpec(spec); + const { key, attr, formatter } = normalized; + const value = formatter ? formatter(this._state[key]) : this._state[key]; + if (attr === 'checked') { + el.checked = !!value; + } else if (attr in el) { + el[attr] = value ?? ''; + } else { + el.setAttribute(attr, value ?? ''); + } + } + } + + _normalizeVarSpec(spec) { + if (typeof spec === 'string') { + return { + key: spec, + attr: 'value', + event: 'input', + parser: null, + formatter: null, + }; + } + const attr = spec.attr || (spec.type === 'checkbox' ? 'checked' : 'value'); + const event = spec.event || (attr === 'checked' ? 'change' : 'input'); + return { + key: spec.key, + attr, + event, + parser: spec.parser || null, + formatter: spec.formatter || null, + }; + } + + _normalizeBindings(bindings) { + if (!bindings) return {}; + if (Array.isArray(bindings)) { + const out = {}; + for (const pair of bindings) { + if (pair && typeof pair.selector === 'string') { + out[pair.selector] = pair.value; + } + } + return out; + } + if (bindings && typeof bindings === 'object') { + return bindings; + } + return {}; + } + + _query(selector) { + if (!selector) return null; + if (this.viewEl) { + return this.viewEl.querySelector(selector); + } + return document.querySelector(selector); + } + + _unbindAll() { + for (const unbind of this._bindings) { + try { unbind(); } catch (_) { /* noop */ } + } + this._bindings = []; + } +} + +export class ModuleBuilder { + constructor() { + this._config = { + name: '', + view: '', + rootSelector: '', + wrapperClass: '', + strict: false, + requiredMethods: [], + bindVars: {}, + bindEvents: {}, + state: {}, + }; + } + + name(value) { + this._config.name = value; + return this; + } + + view(path) { + this._config.view = path; + return this; + } + + root(selector) { + this._config.rootSelector = selector; + return this; + } + + wrapperClass(name) { + this._config.wrapperClass = name; + return this; + } + + strict(flag = true) { + this._config.strict = !!flag; + return this; + } + + required(methods = []) { + this._config.requiredMethods = Array.isArray(methods) ? methods : []; + return this; + } + + state(initialState = {}) { + this._config.state = { ...initialState }; + return this; + } + + bindVar(map = {}) { + this._config.bindVars = this._mergeBindings(this._config.bindVars, map); + return this; + } + + bindEvent(map = {}) { + this._config.bindEvents = this._mergeBindings(this._config.bindEvents, map); + return this; + } + + build() { + if (!this._config.name) { + this._config.name = 'Module'; + } + return { ...this._config }; + } + + _mergeBindings(current, next) { + const base = Array.isArray(current) ? this._pairsToObject(current) : { ...(current || {}) }; + if (Array.isArray(next)) { + return { ...base, ...this._pairsToObject(next) }; + } + if (next && typeof next === 'object') { + return { ...base, ...next }; + } + return base; + } + + _pairsToObject(pairs) { + const out = {}; + for (const pair of pairs) { + if (pair && typeof pair.selector === 'string') { + out[pair.selector] = pair.value; + } + } + return out; + } +} + +export default ModuleBuilder; + +export class BindingPair { + constructor(selector, value) { + if (!selector || typeof selector !== 'string') { + throw new Error('[BindingPair] selector must be a string.'); + } + this.selector = selector; + this.value = value; + } +} diff --git a/SL/module/TableModule.js b/SL/module/TableModule.js new file mode 100644 index 0000000..08ee076 --- /dev/null +++ b/SL/module/TableModule.js @@ -0,0 +1,22 @@ +import { Module, ModuleBuilder } from './Module.js'; +import { bindTableEvents } from '../../ui/table-bindings.js'; + +const builder = new ModuleBuilder() + .name('TableModule') + .view('assets/amily-data-table/Memorisation-forms.html') + .strict(true) + .required(['mount']); + +export default class TableModule extends Module { + constructor() { + super(builder); + } + + async mount() { + if (this.el) { + this.el.id = 'amily2_memorisation_forms_panel'; + this.el.style.display = 'none'; + } + bindTableEvents(); + } +}