ep_hljs 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.cjs +10 -0
- package/.github/workflows/automerge.yml +45 -0
- package/.github/workflows/backend-tests.yml +75 -0
- package/.github/workflows/codeql.yml +41 -0
- package/.github/workflows/frontend-tests.yml +72 -0
- package/.github/workflows/npmpublish.yml +124 -0
- package/.github/workflows/test-and-release.yml +32 -0
- package/CLAUDE.md +141 -0
- package/LICENSE +201 -0
- package/README.md +56 -0
- package/demo.gif +0 -0
- package/demo.png +0 -0
- package/ep.json +26 -0
- package/index.js +109 -0
- package/lib/exportRenderer.js +39 -0
- package/lib/languageAllowlist.js +16 -0
- package/lib/padLanguageStore.js +27 -0
- package/locales/en.json +10 -0
- package/package.json +59 -0
- package/pnpm-workspace.yaml +2 -0
- package/scripts/build-vendor.js +45 -0
- package/static/css/editor.css +94 -0
- package/static/css/themes/github-dark.css +118 -0
- package/static/css/themes/github.css +118 -0
- package/static/js/codeIndent.js +107 -0
- package/static/js/constants.js +7 -0
- package/static/js/domOverlay.js +16 -0
- package/static/js/highlightRegistry.js +109 -0
- package/static/js/hljsAdapter.js +80 -0
- package/static/js/index.js +124 -0
- package/static/js/lruCache.js +28 -0
- package/static/js/syntaxRenderer.js +201 -0
- package/static/js/themeBridge.js +76 -0
- package/static/js/vendor/hljs.min.js +5 -0
- package/static/tests/backend/specs/codeIndent.test.js +144 -0
- package/static/tests/backend/specs/export.test.js +47 -0
- package/static/tests/backend/specs/highlightRegistry.test.js +59 -0
- package/static/tests/backend/specs/hljsAdapter.test.js +43 -0
- package/static/tests/backend/specs/lruCache.test.js +45 -0
- package/static/tests/backend/specs/padLanguageStore.test.js +63 -0
- package/static/tests/backend/specs/socket.test.js +54 -0
- package/static/tests/frontend-new/helper/highlights.ts +64 -0
- package/static/tests/frontend-new/specs/caret-stability.spec.ts +106 -0
- package/static/tests/frontend-new/specs/code-indent.spec.ts +78 -0
- package/static/tests/frontend-new/specs/collaboration.spec.ts +59 -0
- package/static/tests/frontend-new/specs/content-sync.spec.ts +45 -0
- package/static/tests/frontend-new/specs/dark-mode.spec.ts +87 -0
- package/static/tests/frontend-new/specs/export.spec.ts +31 -0
- package/static/tests/frontend-new/specs/initial-paint.spec.ts +49 -0
- package/static/tests/frontend-new/specs/language-picker.spec.ts +54 -0
- package/static/tests/frontend-new/specs/large-pad.spec.ts +36 -0
- package/static/tests/frontend-new/specs/lifecycle.spec.ts +27 -0
- package/static/tests/frontend-new/specs/multi-user-caret.spec.ts +167 -0
- package/static/tests/frontend-new/specs/single-line-while.spec.ts +50 -0
- package/templates/editbarButtons.ejs +29 -0
package/.eslintrc.cjs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// This is a workaround for https://github.com/eslint/eslint/issues/3458
|
|
4
|
+
require('eslint-config-etherpad/patch/modern-module-resolution');
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
root: true,
|
|
8
|
+
extends: 'etherpad/plugin',
|
|
9
|
+
ignorePatterns: ['node_modules', 'static/css/themes', 'static/js/vendor', 'static/tests/frontend-new'],
|
|
10
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: Dependabot Automerge
|
|
2
|
+
permissions:
|
|
3
|
+
contents: write
|
|
4
|
+
pull-requests: write
|
|
5
|
+
# `actions: write` lets the post-merge step kick off Node.js Package on
|
|
6
|
+
# the default branch via `gh workflow run`. Without this, automerge'd
|
|
7
|
+
# PRs land on main but the on-push release job never fires (GitHub
|
|
8
|
+
# Actions intentionally suppresses on:push triggers when the push is
|
|
9
|
+
# authenticated with GITHUB_TOKEN).
|
|
10
|
+
actions: write
|
|
11
|
+
on:
|
|
12
|
+
workflow_run:
|
|
13
|
+
workflows:
|
|
14
|
+
- Node.js Package
|
|
15
|
+
types:
|
|
16
|
+
- completed
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
automerge:
|
|
20
|
+
if: >
|
|
21
|
+
github.event.workflow_run.conclusion == 'success' &&
|
|
22
|
+
github.event.workflow_run.event == 'push' &&
|
|
23
|
+
github.event.workflow_run.actor.login == 'dependabot[bot]'
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
steps:
|
|
26
|
+
- name: Checkout
|
|
27
|
+
uses: actions/checkout@v6
|
|
28
|
+
|
|
29
|
+
- name: Automerge
|
|
30
|
+
id: automerge
|
|
31
|
+
uses: "pascalgn/automerge-action@v0.16.4"
|
|
32
|
+
env:
|
|
33
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
34
|
+
MERGE_METHOD: squash
|
|
35
|
+
MERGE_LABELS: ""
|
|
36
|
+
MERGE_RETRY_SLEEP: "100000"
|
|
37
|
+
|
|
38
|
+
- name: Trigger release on default branch
|
|
39
|
+
# `pascalgn/automerge-action` exits 0 whether or not it merged. Skip
|
|
40
|
+
# the dispatch when nothing was actually merged so we don't kick a
|
|
41
|
+
# phantom release run on every Dependabot Automerge invocation.
|
|
42
|
+
if: steps.automerge.outputs.mergeResult == 'merged'
|
|
43
|
+
env:
|
|
44
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
45
|
+
run: gh workflow run test-and-release.yml --ref ${{ github.event.repository.default_branch }}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
name: Backend Tests
|
|
2
|
+
|
|
3
|
+
# any branch is useful for testing before a PR is submitted
|
|
4
|
+
on:
|
|
5
|
+
workflow_call:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
withplugins:
|
|
9
|
+
# run on pushes to any branch
|
|
10
|
+
# run on PRs from external forks
|
|
11
|
+
if: |
|
|
12
|
+
(github.event_name != 'pull_request')
|
|
13
|
+
|| (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id)
|
|
14
|
+
name: with Plugins
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
-
|
|
18
|
+
name: Install libreoffice
|
|
19
|
+
uses: awalsh128/cache-apt-pkgs-action@v1.6.0
|
|
20
|
+
with:
|
|
21
|
+
packages: libreoffice libreoffice-pdfimport
|
|
22
|
+
version: 1.0
|
|
23
|
+
-
|
|
24
|
+
name: Install etherpad core
|
|
25
|
+
uses: actions/checkout@v6
|
|
26
|
+
with:
|
|
27
|
+
repository: ether/etherpad-lite
|
|
28
|
+
path: etherpad-lite
|
|
29
|
+
- uses: actions/setup-node@v6
|
|
30
|
+
name: Install Node.js
|
|
31
|
+
with:
|
|
32
|
+
node-version: 22
|
|
33
|
+
- uses: pnpm/action-setup@v6
|
|
34
|
+
name: Install pnpm
|
|
35
|
+
with:
|
|
36
|
+
version: 10
|
|
37
|
+
run_install: false
|
|
38
|
+
- name: Get pnpm store directory
|
|
39
|
+
shell: bash
|
|
40
|
+
run: |
|
|
41
|
+
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
|
42
|
+
- uses: actions/cache@v5
|
|
43
|
+
name: Setup pnpm cache
|
|
44
|
+
with:
|
|
45
|
+
path: ${{ env.STORE_PATH }}
|
|
46
|
+
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
|
47
|
+
restore-keys: |
|
|
48
|
+
${{ runner.os }}-pnpm-store-
|
|
49
|
+
-
|
|
50
|
+
name: Checkout plugin repository
|
|
51
|
+
uses: actions/checkout@v6
|
|
52
|
+
with:
|
|
53
|
+
path: plugin
|
|
54
|
+
- name: Remove tests
|
|
55
|
+
working-directory: ./etherpad-lite
|
|
56
|
+
run: rm -rf ./src/tests/backend/specs
|
|
57
|
+
-
|
|
58
|
+
name: Install Etherpad core dependencies
|
|
59
|
+
working-directory: ./etherpad-lite
|
|
60
|
+
run: bin/installDeps.sh
|
|
61
|
+
- name: Install plugin
|
|
62
|
+
working-directory: ./etherpad-lite
|
|
63
|
+
run: |
|
|
64
|
+
pnpm run plugins i --path ../../plugin
|
|
65
|
+
-
|
|
66
|
+
name: Run the backend tests
|
|
67
|
+
working-directory: ./etherpad-lite/src
|
|
68
|
+
run: |
|
|
69
|
+
shopt -s globstar
|
|
70
|
+
res=$(find ./plugin_packages -path "*/static/tests/backend/specs/*" 2>/dev/null | wc -l)
|
|
71
|
+
if [ $res -eq 0 ]; then
|
|
72
|
+
echo "No backend tests found"
|
|
73
|
+
else
|
|
74
|
+
npx cross-env NODE_ENV=production mocha --import=tsx --timeout 120000 --recursive node_modules/ep_*/static/tests/backend/specs/**
|
|
75
|
+
fi
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: "CodeQL"
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ "main" ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ "main" ]
|
|
8
|
+
schedule:
|
|
9
|
+
- cron: "26 5 * * 2"
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
analyze:
|
|
13
|
+
name: Analyze
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
permissions:
|
|
16
|
+
actions: read
|
|
17
|
+
contents: read
|
|
18
|
+
security-events: write
|
|
19
|
+
|
|
20
|
+
strategy:
|
|
21
|
+
fail-fast: false
|
|
22
|
+
matrix:
|
|
23
|
+
language: [ javascript ]
|
|
24
|
+
|
|
25
|
+
steps:
|
|
26
|
+
- name: Checkout
|
|
27
|
+
uses: actions/checkout@v6
|
|
28
|
+
|
|
29
|
+
- name: Initialize CodeQL
|
|
30
|
+
uses: github/codeql-action/init@v4
|
|
31
|
+
with:
|
|
32
|
+
languages: ${{ matrix.language }}
|
|
33
|
+
queries: +security-and-quality
|
|
34
|
+
|
|
35
|
+
- name: Autobuild
|
|
36
|
+
uses: github/codeql-action/autobuild@v4
|
|
37
|
+
|
|
38
|
+
- name: Perform CodeQL Analysis
|
|
39
|
+
uses: github/codeql-action/analyze@v4
|
|
40
|
+
with:
|
|
41
|
+
category: "/language:${{ matrix.language }}"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Publicly credit Sauce Labs because they generously support open source
|
|
2
|
+
# projects.
|
|
3
|
+
name: Frontend Tests
|
|
4
|
+
|
|
5
|
+
on:
|
|
6
|
+
workflow_call:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test-frontend:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
|
|
12
|
+
steps:
|
|
13
|
+
-
|
|
14
|
+
name: Check out Etherpad core
|
|
15
|
+
uses: actions/checkout@v6
|
|
16
|
+
with:
|
|
17
|
+
repository: ether/etherpad-lite
|
|
18
|
+
path: etherpad-lite
|
|
19
|
+
- uses: actions/setup-node@v6
|
|
20
|
+
name: Install Node.js
|
|
21
|
+
with:
|
|
22
|
+
node-version: 22
|
|
23
|
+
- uses: pnpm/action-setup@v6
|
|
24
|
+
name: Install pnpm
|
|
25
|
+
with:
|
|
26
|
+
version: 10
|
|
27
|
+
run_install: false
|
|
28
|
+
- name: Get pnpm store directory
|
|
29
|
+
shell: bash
|
|
30
|
+
run: |
|
|
31
|
+
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
|
32
|
+
- uses: actions/cache@v5
|
|
33
|
+
name: Setup pnpm cache
|
|
34
|
+
with:
|
|
35
|
+
path: ${{ env.STORE_PATH }}
|
|
36
|
+
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
|
37
|
+
restore-keys: |
|
|
38
|
+
${{ runner.os }}-pnpm-store-
|
|
39
|
+
-
|
|
40
|
+
name: Checkout plugin repository
|
|
41
|
+
uses: actions/checkout@v6
|
|
42
|
+
with:
|
|
43
|
+
path: plugin
|
|
44
|
+
-
|
|
45
|
+
name: Install Etherpad core dependencies
|
|
46
|
+
working-directory: ./etherpad-lite
|
|
47
|
+
run: bin/installDeps.sh
|
|
48
|
+
- name: Install plugin
|
|
49
|
+
working-directory: ./etherpad-lite
|
|
50
|
+
run: |
|
|
51
|
+
pnpm run plugins i --path ../../plugin
|
|
52
|
+
- name: Create settings.json
|
|
53
|
+
working-directory: ./etherpad-lite
|
|
54
|
+
run: cp ./src/tests/settings.json settings.json
|
|
55
|
+
- name: Run the frontend tests
|
|
56
|
+
working-directory: ./etherpad-lite
|
|
57
|
+
shell: bash
|
|
58
|
+
run: |
|
|
59
|
+
pnpm run dev &
|
|
60
|
+
connected=false
|
|
61
|
+
can_connect() {
|
|
62
|
+
curl -sSfo /dev/null http://localhost:9001/ || return 1
|
|
63
|
+
connected=true
|
|
64
|
+
}
|
|
65
|
+
now() { date +%s; }
|
|
66
|
+
start=$(now)
|
|
67
|
+
while [ $(($(now) - $start)) -le 30 ] && ! can_connect; do
|
|
68
|
+
sleep 1
|
|
69
|
+
done
|
|
70
|
+
cd src
|
|
71
|
+
pnpm exec playwright install chromium --with-deps
|
|
72
|
+
pnpm run test-ui --project=chromium
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# This workflow will run tests using node and then publish a package to the npm registry when a release is created
|
|
2
|
+
# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
|
|
3
|
+
#
|
|
4
|
+
# Publishing uses npm Trusted Publishing (OIDC) — no NPM_TOKEN secret is
|
|
5
|
+
# required. Each package must have a trusted publisher configured on npmjs.com
|
|
6
|
+
# pointing at this workflow file. See:
|
|
7
|
+
# https://docs.npmjs.com/trusted-publishers
|
|
8
|
+
|
|
9
|
+
name: Node.js Package
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
workflow_call:
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
publish-npm:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
permissions:
|
|
18
|
+
contents: write # for the atomic version-bump push (branch + tag)
|
|
19
|
+
id-token: write # for npm OIDC trusted publishing
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/setup-node@v6
|
|
22
|
+
with:
|
|
23
|
+
# OIDC trusted publishing needs npm >= 11.5.1, which requires
|
|
24
|
+
# Node >= 20.17.0. setup-node's `20` resolves to the latest
|
|
25
|
+
# 20.x, which satisfies that.
|
|
26
|
+
node-version: 20
|
|
27
|
+
registry-url: https://registry.npmjs.org/
|
|
28
|
+
- name: Upgrade npm to >=11.5.1 (required for trusted publishing)
|
|
29
|
+
run: npm install -g npm@latest
|
|
30
|
+
- name: Check out Etherpad core
|
|
31
|
+
uses: actions/checkout@v6
|
|
32
|
+
with:
|
|
33
|
+
repository: ether/etherpad-lite
|
|
34
|
+
- uses: pnpm/action-setup@v6
|
|
35
|
+
name: Install pnpm
|
|
36
|
+
with:
|
|
37
|
+
# No `version:` here — defer to packageManager in package.json so
|
|
38
|
+
# we don't clash with etherpad-lite's pnpm version pin (the
|
|
39
|
+
# checkout above puts its package.json at cwd). See
|
|
40
|
+
# pnpm/action-setup#225.
|
|
41
|
+
run_install: false
|
|
42
|
+
- name: Get pnpm store directory
|
|
43
|
+
shell: bash
|
|
44
|
+
run: |
|
|
45
|
+
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
|
46
|
+
- uses: actions/cache@v5
|
|
47
|
+
name: Setup pnpm cache
|
|
48
|
+
with:
|
|
49
|
+
path: ${{ env.STORE_PATH }}
|
|
50
|
+
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
|
51
|
+
restore-keys: |
|
|
52
|
+
${{ runner.os }}-pnpm-store-
|
|
53
|
+
-
|
|
54
|
+
uses: actions/checkout@v6
|
|
55
|
+
with:
|
|
56
|
+
fetch-depth: 0
|
|
57
|
+
-
|
|
58
|
+
name: Bump version (patch)
|
|
59
|
+
id: bump
|
|
60
|
+
run: |
|
|
61
|
+
LATEST_TAG=$(git describe --tags --abbrev=0) || exit 1
|
|
62
|
+
NEW_COMMITS=$(git rev-list --count "${LATEST_TAG}"..) || exit 1
|
|
63
|
+
# No new commits since the last tag → nothing to publish.
|
|
64
|
+
# Setting `bumped=false` lets the publish step below skip
|
|
65
|
+
# itself instead of trying to republish the existing version
|
|
66
|
+
# (which fails with "cannot publish over previously published
|
|
67
|
+
# versions"). Only manual `gh workflow run` invocations on a
|
|
68
|
+
# tag-current branch hit this path; on:push always sees ≥1
|
|
69
|
+
# new commit because the push event is what triggered us.
|
|
70
|
+
if [ "${NEW_COMMITS}" -le 0 ]; then
|
|
71
|
+
echo "bumped=false" >> "${GITHUB_OUTPUT}"
|
|
72
|
+
exit 0
|
|
73
|
+
fi
|
|
74
|
+
git config user.name 'github-actions[bot]'
|
|
75
|
+
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
|
|
76
|
+
pnpm i --frozen-lockfile
|
|
77
|
+
# `pnpm version patch` bumps package.json, makes a commit, and creates
|
|
78
|
+
# a `v<new-version>` tag. Capture the new tag name from package.json
|
|
79
|
+
# rather than parsing pnpm's output, which has historically varied.
|
|
80
|
+
# Bump the patch component directly with Node. pnpm/action-setup@v6
|
|
81
|
+
# sometimes installs pnpm 11 pre-releases even when version: 10.x is
|
|
82
|
+
# requested (pnpm/action-setup#225); those pre-releases either skip
|
|
83
|
+
# the git commit/tag or reject --no-git-tag-version as unknown.
|
|
84
|
+
# Doing the bump in Node sidesteps both failure modes.
|
|
85
|
+
NEW_VERSION=$(node -e "const fs=require('fs');const p=require('./package.json');const v=p.version.split('.');v[2]=String(Number(v[2])+1);p.version=v.join('.');fs.writeFileSync('./package.json',JSON.stringify(p,null,2)+'\n');console.log(p.version);")
|
|
86
|
+
NEW_TAG="v${NEW_VERSION}"
|
|
87
|
+
git add package.json
|
|
88
|
+
git commit -m "${NEW_TAG}"
|
|
89
|
+
git tag -a "${NEW_TAG}" -m "${NEW_TAG}"
|
|
90
|
+
# CRITICAL: use --atomic so the branch update and the tag update
|
|
91
|
+
# succeed (or fail) as a single transaction on the server. The old
|
|
92
|
+
# `git push --follow-tags` was non-atomic per ref: if a concurrent
|
|
93
|
+
# publish run won the race, the branch fast-forward would be rejected
|
|
94
|
+
# but the tag push would still land — leaving a dangling tag with no
|
|
95
|
+
# matching commit on the branch. Subsequent runs would then forever
|
|
96
|
+
# try to bump to the same already-existing tag and fail with
|
|
97
|
+
# `tag 'vN+1' already exists`. With --atomic, a rejected branch push
|
|
98
|
+
# rejects the tag push too, and the next workflow tick can retry
|
|
99
|
+
# cleanly against the up-to-date refs.
|
|
100
|
+
git push --atomic origin "${GITHUB_REF_NAME}" "${NEW_TAG}"
|
|
101
|
+
echo "bumped=true" >> "${GITHUB_OUTPUT}"
|
|
102
|
+
# This is required if the package has a prepare script that uses something
|
|
103
|
+
# in dependencies or devDependencies.
|
|
104
|
+
-
|
|
105
|
+
if: steps.bump.outputs.bumped == 'true'
|
|
106
|
+
run: pnpm i
|
|
107
|
+
# `npm publish` must come after `git push` otherwise there is a race
|
|
108
|
+
# condition: If two PRs are merged back-to-back then master/main will be
|
|
109
|
+
# updated with the commits from the second PR before the first PR's
|
|
110
|
+
# workflow has a chance to push the commit generated by `npm version
|
|
111
|
+
# patch`. This causes the first PR's `git push` step to fail after the
|
|
112
|
+
# package has already been published, which in turn will cause all future
|
|
113
|
+
# workflow runs to fail because they will all attempt to use the same
|
|
114
|
+
# already-used version number. By running `npm publish` after `git push`,
|
|
115
|
+
# back-to-back merges will cause the first merge's workflow to fail but
|
|
116
|
+
# the second's will succeed.
|
|
117
|
+
#
|
|
118
|
+
# Use `npm publish` directly (not `pnpm publish`) because OIDC trusted
|
|
119
|
+
# publishing requires npm CLI >= 11.5.1 and `pnpm publish` shells out to
|
|
120
|
+
# whichever `npm` is on PATH; calling `npm` directly avoids any shim
|
|
121
|
+
# ambiguity.
|
|
122
|
+
- name: Publish to npm via OIDC
|
|
123
|
+
if: steps.bump.outputs.bumped == 'true'
|
|
124
|
+
run: npm publish --provenance --access public
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: Node.js Package
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
# Invoked by automerge.yml after a Dependabot PR is merged. GitHub
|
|
5
|
+
# Actions doesn't fire on:push when the push is authored by GITHUB_TOKEN
|
|
6
|
+
# (the automerge action's only available identity), so without this
|
|
7
|
+
# dispatch trigger the release job never runs after auto-merges.
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
# id-token: write must be granted here so the reusable npmpublish workflow
|
|
11
|
+
# can request an OIDC token for npm trusted publishing.
|
|
12
|
+
permissions:
|
|
13
|
+
contents: write
|
|
14
|
+
id-token: write
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
backend:
|
|
18
|
+
uses: ./.github/workflows/backend-tests.yml
|
|
19
|
+
secrets: inherit
|
|
20
|
+
frontend:
|
|
21
|
+
uses: ./.github/workflows/frontend-tests.yml
|
|
22
|
+
secrets: inherit
|
|
23
|
+
release:
|
|
24
|
+
if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' }}
|
|
25
|
+
needs:
|
|
26
|
+
- backend
|
|
27
|
+
- frontend
|
|
28
|
+
permissions:
|
|
29
|
+
contents: write # for the version bump push
|
|
30
|
+
id-token: write # for npm OIDC trusted publishing
|
|
31
|
+
uses: ./.github/workflows/npmpublish.yml
|
|
32
|
+
secrets: inherit
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Claude Code Guidelines for `ep_hljs`
|
|
2
|
+
|
|
3
|
+
Whole-pad syntax highlighting for Etherpad, powered by highlight.js, painted via the **CSS Custom Highlights API**. This file is the architectural reference for anyone (Claude or human) extending the plugin.
|
|
4
|
+
|
|
5
|
+
## Architecture in one paragraph
|
|
6
|
+
|
|
7
|
+
Tokens are computed at render time and painted by the browser via `CSS.highlights` Range registration — **the DOM Etherpad's Ace owns is never mutated**. On every line render (`acePostWriteDomLineHTML`) and every text mutation (`MutationObserver`), the renderer reads `node.textContent`, runs `hljs.highlight(text, {language})` (cached in an LRU keyed by `language:text`), turns the resulting `<span>`-soup into `{start, end, cls}` token ranges, then builds DOM `Range` objects pointing into the line's existing text nodes and adds them to `CSS.highlights.get('hljs-keyword')` etc. The browser composites the paint via `::highlight(hljs-…)` rules in `editor.css`. No `splitText`, no injected `<span>`s, no Easysync attribute broadcast.
|
|
8
|
+
|
|
9
|
+
## Files
|
|
10
|
+
|
|
11
|
+
| Path | Role |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `static/js/syntaxRenderer.js` | Orchestrator: state, LRU cache, auto-detect timer, `acePostWriteDomLineHTML` hook, MutationObserver, repaintAllLines |
|
|
14
|
+
| `static/js/highlightRegistry.js` | Wraps `CSS.highlights`. `setLineRanges(lineEl, [{start,end,cls}])` / `removeLineRanges(lineEl)` / `clearAll()` |
|
|
15
|
+
| `static/js/hljsAdapter.js` | `tokenize(text, lang) -> ranges \| null`, `detect(text) -> language \| null`, `parseHljsHtml(html) -> ranges` |
|
|
16
|
+
| `static/js/codeIndent.js` | `aceKeyEvent` handler: Enter inherits/extends indent, Tab/Shift+Tab indent/de-indent in code mode |
|
|
17
|
+
| `static/js/lruCache.js` | Map-backed LRU |
|
|
18
|
+
| `static/js/index.js` | Client postAceInit: dynamically injects `vendor/hljs.min.js`, wires socketio + padToggle.init + syntaxRenderer.start + codeIndent.start |
|
|
19
|
+
| `static/js/themeBridge.js` | Light/dark theme detection (no longer swaps stylesheets — `editor.css` carries both palettes) |
|
|
20
|
+
| `static/js/domOverlay.js` | "Highlighting paused" badge for MAX_LINES kill switch |
|
|
21
|
+
| `static/css/editor.css` | `::highlight(hljs-…)` rules. Light + dark palettes scoped via `.super-dark-editor` |
|
|
22
|
+
| `static/css/themes/{github,github-dark}.css` | Vendored hljs themes — used **only by export pipeline**, not by the live editor |
|
|
23
|
+
| `index.js` (server) | loadSettings, clientVars (lang + indentSize), socketio (setLanguage), eejsBlock_*, getLineHTMLForExport, stylesForExport |
|
|
24
|
+
| `lib/padLanguageStore.js` | `{language, autoDetect}` per pad |
|
|
25
|
+
| `lib/exportRenderer.js` | HTML/PDF export: hljs.highlight() emits real `<span>`s + theme CSS inlined |
|
|
26
|
+
|
|
27
|
+
## How rendering actually flows
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
keystroke / paste / remote changeset
|
|
31
|
+
│
|
|
32
|
+
▼
|
|
33
|
+
Etherpad updates inner-iframe DOM
|
|
34
|
+
│
|
|
35
|
+
├─ acePostWriteDomLineHTML hook fires (full line re-renders only)
|
|
36
|
+
│ └─ syntaxRenderer.renderLine(lineDiv)
|
|
37
|
+
│
|
|
38
|
+
└─ MutationObserver fires (every text mutation, including incremental typing)
|
|
39
|
+
└─ syntaxRenderer.renderLine(dirtyLine)
|
|
40
|
+
│
|
|
41
|
+
▼
|
|
42
|
+
tokenize(text, language) ← LRU cache
|
|
43
|
+
│
|
|
44
|
+
▼
|
|
45
|
+
setLineRanges(lineEl, ranges)
|
|
46
|
+
│
|
|
47
|
+
├─ removeLineRanges(lineEl) // strip old Highlight refs
|
|
48
|
+
├─ buildSegments(lineEl) // walk text nodes
|
|
49
|
+
├─ buildRange(...) per token // Range with setStart/setEnd
|
|
50
|
+
└─ CSS.highlights.get(cls).add(range)
|
|
51
|
+
│
|
|
52
|
+
▼
|
|
53
|
+
browser paints ::highlight(cls) rules
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Hard-won lessons (read before changing the render path)
|
|
57
|
+
|
|
58
|
+
### 1. Don't mutate the DOM Ace owns
|
|
59
|
+
v0.1 stored tokens as Easysync attributes — `setAttributesOnRange` moved the caret to the start of the range on the active line. v0.2 wrapped text nodes via `splitText`/`insertBefore` — fought Etherpad's `_magicdom_dirtiness.knownHTML` bookkeeping; lying about `knownHTML` to break the feedback loop broke remote changeset application. **CSS Custom Highlights is the answer:** observation only, no DOM modification, no fights with Ace.
|
|
60
|
+
|
|
61
|
+
### 2. `acePostWriteDomLineHTML` only fires on FULL line re-renders
|
|
62
|
+
Etherpad does incremental DOM updates on single-character typing (modifies `textNode.nodeValue` in place). The hook is only called when a line is fully written: paste, language change, line split, undo/redo. To catch every text mutation, install a `MutationObserver` on the inner doc body (`{childList: true, subtree: true, characterData: true}`) and walk up to the magicdomid line div. Etherpad sometimes swaps a line div wholesale on edit — `m.target` becomes `innerdocbody` and the new line is in `m.addedNodes`, so iterate both `m.target` and `m.addedNodes` when finding line ancestors.
|
|
63
|
+
|
|
64
|
+
### 3. The cache must not poison
|
|
65
|
+
`tokenize()` returns `null` (not `[]`) when `window.hljs` isn't loaded. `renderLine` defers — does not cache, does not strip existing highlights. If you cache `[]` from a missing-hljs render, every future render for that key is empty.
|
|
66
|
+
|
|
67
|
+
### 4. `CSS.highlights` is per-document
|
|
68
|
+
The editor lives in a nested iframe (`ace_outer` → `ace_inner`). Range objects must be created on the inner document and Highlights registered with the inner window's `CSS.highlights`. Using the outer window's `Highlight` constructor or `CSS.highlights` produces silent no-ops. `highlightRegistry.setLineRanges` resolves the right window via `lineEl.ownerDocument.defaultView`.
|
|
69
|
+
|
|
70
|
+
### 5. Multi-class hljs spans need splitting
|
|
71
|
+
hljs sometimes emits `<span class="hljs-meta hljs-string">@foo</span>`. CSS `<custom-ident>` doesn't allow spaces, so registering a Highlight named `"hljs-meta hljs-string"` is invisible. `parseHljsHtml` splits on whitespace and emits one range per class.
|
|
72
|
+
|
|
73
|
+
### 6. Etherpad applies `super-dark-editor` to the inner `<html>`, not `<body>`
|
|
74
|
+
Per `ace.ts:266`. Our dark-mode CSS uses `.super-dark-editor ::highlight(…)` (descendant-selector) so it matches whether the class is on `html`, `body`, or any ancestor.
|
|
75
|
+
|
|
76
|
+
### 7. `padToggle` exposes `init({onChange})`, not `subscribe`
|
|
77
|
+
The helper's eejs templates render `<input type="checkbox">` with NO `checked` attribute. Without calling `init()` on the client, the checkbox stays unchecked even when `defaultEnabled: true`. The flow is server-renders-empty → client-init-binds-state.
|
|
78
|
+
|
|
79
|
+
### 8. `acePostWriteDomLineHTML` fires BEFORE `postAceInit` for the initial render
|
|
80
|
+
For pads with persisted language, the initial line renders happen before `loadHljs()` resolves and before `syntaxRenderer.start()` sets state. The hook short-circuits on auto/missing-hljs and leaves them un-tokenized. Fix: schedule a `setTimeout(repaintAllLines, 100)` from `start()` to catch lines that rendered too early.
|
|
81
|
+
|
|
82
|
+
### 9. Auto-detect is per-client, no broadcast (yet)
|
|
83
|
+
Each client runs `hljs.highlightAuto(padText)` on a 2s idle interval. Convergence relies on `hljs.highlightAuto` being deterministic (same content → same language). If divergence is reported, add a jittered `setLanguage` broadcast with cancel-on-incoming so first-fire wins (sketched but not landed; see git stash `v0.2-task6-wip-archive` for the broadcast variant).
|
|
84
|
+
|
|
85
|
+
### 10. The `padShortcutEnabled.tab` Etherpad setting affects Tab interception
|
|
86
|
+
`codeIndent.handleKey` returns `true` and `evt.preventDefault()` to suppress Etherpad's default Tab. Test before assuming this works for a given keystroke — `aceKeyEvent` hook ordering means `outsideKeyPress` runs *before* the hook for Enter on Chrome (`isTypeForSpecialKey && keyCode === 13` branch in `ace2_inner.ts`). Backend unit tests are reliable; some Playwright tests for keystroke interaction are flaky and `test.fixme`'d.
|
|
87
|
+
|
|
88
|
+
## Settings (server-side)
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
"ep_hljs": {
|
|
92
|
+
"indentSize": 2
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`indentSize` is admin-configurable in `settings.json` (clamped to `1..16`, default `2`). A pad-level UI picker (TOC-style) is a deferred follow-up.
|
|
97
|
+
|
|
98
|
+
## i18n
|
|
99
|
+
|
|
100
|
+
All user-facing strings have `data-l10n-id` and a fallback English string. Keys live in `locales/en.json`:
|
|
101
|
+
|
|
102
|
+
| Key | Where |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `ep_hljs.label` | toolbar dropdown aria-label |
|
|
105
|
+
| `ep_hljs.auto` | dropdown "Auto-detect" option |
|
|
106
|
+
| `ep_hljs.off` | dropdown "Off" option |
|
|
107
|
+
| `ep_hljs.paused` | "Highlighting paused" badge |
|
|
108
|
+
| `ep_hljs.user_enable` | padToggle checkbox label |
|
|
109
|
+
|
|
110
|
+
Programming language names (`JavaScript`, `Python`, etc.) are intentionally **not** translated — they're proper nouns / fixed identifiers in the hljs grammar registry.
|
|
111
|
+
|
|
112
|
+
## Accessibility
|
|
113
|
+
|
|
114
|
+
- The toolbar `<select>` exposes `aria-label` (via `data-l10n-attr="aria-label"`).
|
|
115
|
+
- Toggle checkboxes get `<label for="...">` from the padToggle helper.
|
|
116
|
+
- `Ctrl/Alt/Meta + Tab` is **not** intercepted by `codeIndent` — keyboard navigation escape hatch.
|
|
117
|
+
- `Shift+Tab` defers to Etherpad's default when there's no leading whitespace to remove (so list-mode Shift+Tab still works).
|
|
118
|
+
- **Color contrast trade-off:** the light-mode operator color (`#3b82f6` against white) is ~3.7:1 — passes WCAG AA for large/bold text only. The string color (`#1d4ed8`) passes AA at 7:1. Users with low-vision needs should use the dark skin variant which has high-contrast brights (`#79c0ff` etc., > 7:1 on the dark BG).
|
|
119
|
+
|
|
120
|
+
## Tests
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# Backend (jsdom + mocha)
|
|
124
|
+
cd etherpad-lite/src && npx cross-env NODE_ENV=production mocha --import=tsx --timeout 30000 \
|
|
125
|
+
$(find plugin_packages -path '*ep_hljs*/static/tests/backend/specs/*.test.js' | tr '\n' ' ')
|
|
126
|
+
|
|
127
|
+
# Frontend (Playwright)
|
|
128
|
+
cd etherpad-lite/src && CI=true pnpm exec playwright test --project=chromium --reporter=line --workers=1 --retries=0 \
|
|
129
|
+
$(find plugin_packages -path '*ep_hljs*/static/tests/frontend-new/specs/*.spec.ts' | tr '\n' ' ')
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Backend (38 cases): `lruCache`, `highlightRegistry` (jsdom Range building), `codeIndent` (Enter/Tab/Shift+Tab logic with mocked rep+editorInfo), `hljsAdapter` (parseHljsHtml multi-class + entities), `padLanguageStore`, `socket`, `export`.
|
|
133
|
+
|
|
134
|
+
Frontend (~21 cases, ~3 fixme'd): `caret-stability`, `collaboration`, `content-sync`, `dark-mode` (CSS rule presence), `export`, `initial-paint`, `language-picker`, `large-pad`, `lifecycle`, `multi-user-caret`, `single-line-while`, `code-indent` (mostly fixme — see lesson #10).
|
|
135
|
+
|
|
136
|
+
## Known issues / follow-ups
|
|
137
|
+
|
|
138
|
+
- Pad-level indent size picker (currently admin-only via `settings.json`).
|
|
139
|
+
- Auto-detect divergence between collaborators is theoretically possible; broadcast-with-jitter pattern designed but not landed (see git stash).
|
|
140
|
+
- `code-indent.spec.ts` Enter/Tab/Shift+Tab Playwright cases are `test.fixme` — manual testing + backend unit tests cover the logic.
|
|
141
|
+
- `caret-stability.spec.ts` and `multi-user-caret.spec.ts` repro 2 are intermittently flaky on the niceSelect dropdown click — UI timing race, not a code bug.
|