Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Claude Code Guidelines for `ep_syntax_highlighting`
# Claude Code Guidelines for `ep_hljs`

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.

Expand Down Expand Up @@ -88,7 +88,7 @@ Each client runs `hljs.highlightAuto(padText)` on a 2s idle interval. Convergenc
## Settings (server-side)

```json
"ep_syntax_highlighting": {
"ep_hljs": {
"indentSize": 2
}
```
Expand All @@ -101,11 +101,11 @@ All user-facing strings have `data-l10n-id` and a fallback English string. Keys

| Key | Where |
|---|---|
| `ep_syntax_highlighting.label` | toolbar dropdown aria-label |
| `ep_syntax_highlighting.auto` | dropdown "Auto-detect" option |
| `ep_syntax_highlighting.off` | dropdown "Off" option |
| `ep_syntax_highlighting.paused` | "Highlighting paused" badge |
| `ep_syntax_highlighting.user_enable` | padToggle checkbox label |
| `ep_hljs.label` | toolbar dropdown aria-label |
| `ep_hljs.auto` | dropdown "Auto-detect" option |
| `ep_hljs.off` | dropdown "Off" option |
| `ep_hljs.paused` | "Highlighting paused" badge |
| `ep_hljs.user_enable` | padToggle checkbox label |

Programming language names (`JavaScript`, `Python`, etc.) are intentionally **not** translated — they're proper nouns / fixed identifiers in the hljs grammar registry.

Expand All @@ -122,11 +122,11 @@ Programming language names (`JavaScript`, `Python`, etc.) are intentionally **no
```bash
# Backend (jsdom + mocha)
cd etherpad-lite/src && npx cross-env NODE_ENV=production mocha --import=tsx --timeout 30000 \
$(find plugin_packages -path '*ep_syntax_highlighting*/static/tests/backend/specs/*.test.js' | tr '\n' ' ')
$(find plugin_packages -path '*ep_hljs*/static/tests/backend/specs/*.test.js' | tr '\n' ' ')

# Frontend (Playwright)
cd etherpad-lite/src && CI=true pnpm exec playwright test --project=chromium --reporter=line --workers=1 --retries=0 \
$(find plugin_packages -path '*ep_syntax_highlighting*/static/tests/frontend-new/specs/*.spec.ts' | tr '\n' ' ')
$(find plugin_packages -path '*ep_hljs*/static/tests/frontend-new/specs/*.spec.ts' | tr '\n' ' ')
```

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`.
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# ep_syntax_highlighting
# ep_hljs

Whole-pad syntax highlighting for Etherpad, powered by [highlight.js](https://highlightjs.org/). Closes [ether/etherpad#6616](https://github.com/ether/etherpad/issues/6616).

Expand Down Expand Up @@ -26,15 +26,15 @@ Highlight.js detection runs on a 2-second idle timer; the LRU-cached `hljs.highl
## Install

```bash
pnpm run plugins i ep_syntax_highlighting
pnpm run plugins i ep_hljs
```

## Configure

Optional admin overrides in `settings.json`:

```json
"ep_syntax_highlighting": {
"ep_hljs": {
"indent-size": 4
}
```
Expand All @@ -53,4 +53,4 @@ On older browsers the editor still works — highlighting silently no-ops.

## Bugs / requests

[github.com/ether/ep_syntax_highlighting/issues](https://github.com/ether/ep_syntax_highlighting/issues)
[github.com/ether/ep_hljs/issues](https://github.com/ether/ep_hljs/issues)
30 changes: 15 additions & 15 deletions ep.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,23 @@
{
"name": "main",
"client_hooks": {
"postAceInit": "ep_syntax_highlighting/static/js/index",
"aceEditorCSS": "ep_syntax_highlighting/static/js/index",
"acePostWriteDomLineHTML": "ep_syntax_highlighting/static/js/index",
"aceKeyEvent": "ep_syntax_highlighting/static/js/index",
"handleClientMessage_CLIENT_MESSAGE": "ep_syntax_highlighting/static/js/index"
"postAceInit": "ep_hljs/static/js/index",
"aceEditorCSS": "ep_hljs/static/js/index",
"acePostWriteDomLineHTML": "ep_hljs/static/js/index",
"aceKeyEvent": "ep_hljs/static/js/index",
"handleClientMessage_CLIENT_MESSAGE": "ep_hljs/static/js/index"
},
"hooks": {
"loadSettings": "ep_syntax_highlighting/index",
"padRemove": "ep_syntax_highlighting/index",
"padCopy": "ep_syntax_highlighting/index",
"clientVars": "ep_syntax_highlighting/index",
"socketio": "ep_syntax_highlighting/index",
"eejsBlock_editbarMenuLeft": "ep_syntax_highlighting/index",
"eejsBlock_mySettings": "ep_syntax_highlighting/index",
"eejsBlock_padSettings": "ep_syntax_highlighting/index",
"getLineHTMLForExport": "ep_syntax_highlighting/index",
"stylesForExport": "ep_syntax_highlighting/index"
"loadSettings": "ep_hljs/index",
"padRemove": "ep_hljs/index",
"padCopy": "ep_hljs/index",
"clientVars": "ep_hljs/index",
"socketio": "ep_hljs/index",
"eejsBlock_editbarMenuLeft": "ep_hljs/index",
"eejsBlock_mySettings": "ep_hljs/index",
"eejsBlock_padSettings": "ep_hljs/index",
"getLineHTMLForExport": "ep_hljs/index",
"stylesForExport": "ep_hljs/index"
}
}
]
Expand Down
16 changes: 8 additions & 8 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,22 @@ const {padSelect} = require('ep_plugin_helpers/pad-select-server');
// "Highlight syntax in pads" toggle. Helper owns checkbox rendering, cookie
// persistence, broadcast/sync, enforceSettings, and i18n wiring.
const highlightToggle = padToggle({
pluginName: 'ep_syntax_highlighting',
pluginName: 'ep_hljs',
settingId: 'syntax-highlighting',
l10nId: 'ep_syntax_highlighting.user_enable',
l10nId: 'ep_hljs.user_enable',
defaultLabel: 'Highlight syntax in pads',
defaultEnabled: true,
});

// Indent size dropdown (2 vs 4 spaces) for code-mode indenting.
const indentSelect = padSelect({
pluginName: 'ep_syntax_highlighting',
pluginName: 'ep_hljs',
settingId: 'indent-size',
l10nId: 'ep_syntax_highlighting.indent_size',
l10nId: 'ep_hljs.indent_size',
defaultLabel: 'Indent size',
options: [
{value: 2, label: '2 spaces', l10nId: 'ep_syntax_highlighting.indent_2'},
{value: 4, label: '4 spaces', l10nId: 'ep_syntax_highlighting.indent_4'},
{value: 2, label: '2 spaces', l10nId: 'ep_hljs.indent_2'},
{value: 4, label: '4 spaces', l10nId: 'ep_hljs.indent_4'},
],
defaultValue: 2,
});
Expand Down Expand Up @@ -74,7 +74,7 @@ exports.clientVars = async (hook, context) => {
...((toggleVars && toggleVars.ep_plugin_helpers) || {}),
...((indentVars && indentVars.ep_plugin_helpers) || {}),
},
ep_syntax_highlighting: value,
ep_hljs: value,
};
};

Expand All @@ -97,7 +97,7 @@ exports.socketio = (hookName, {io}) => {
};

exports.eejsBlock_editbarMenuLeft = (hookName, args, cb) => {
args.content += eejs.require('ep_syntax_highlighting/templates/editbarButtons.ejs', {}, module);
args.content += eejs.require('ep_hljs/templates/editbarButtons.ejs', {}, module);
cb();
};

Expand Down
16 changes: 8 additions & 8 deletions locales/en.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"ep_syntax_highlighting.label": "Syntax language",
"ep_syntax_highlighting.auto": "Auto-detect",
"ep_syntax_highlighting.off": "Off (no highlighting)",
"ep_syntax_highlighting.paused": "Highlighting paused (pad too large)",
"ep_syntax_highlighting.user_enable": "Highlight syntax in pads",
"ep_syntax_highlighting.indent_size": "Indent size",
"ep_syntax_highlighting.indent_2": "2 spaces",
"ep_syntax_highlighting.indent_4": "4 spaces"
"ep_hljs.label": "Syntax language",
"ep_hljs.auto": "Auto-detect",
"ep_hljs.off": "Off (no highlighting)",
"ep_hljs.paused": "Highlighting paused (pad too large)",
"ep_hljs.user_enable": "Highlight syntax in pads",
"ep_hljs.indent_size": "Indent size",
"ep_hljs.indent_2": "2 spaces",
"ep_hljs.indent_4": "4 spaces"
}
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
{
"name": "ep_syntax_highlighting",
"name": "ep_hljs",
"version": "0.1.1",
"description": "Whole-pad syntax highlighting for Etherpad via highlight.js. Auto-detect or pick a language; tokens are painted via the CSS Custom Highlights API — no DOM mutation, no caret drift.",
"license": "Apache-2.0",
"author": {
"name": "John McLear",
"email": "john@mclear.co.uk"
},
"homepage": "https://github.com/ether/ep_syntax_highlighting#readme",
"homepage": "https://github.com/ether/ep_hljs#readme",
"bugs": {
"url": "https://github.com/ether/ep_syntax_highlighting/issues"
"url": "https://github.com/ether/ep_hljs/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/ether/ep_syntax_highlighting.git"
"url": "https://github.com/ether/ep_hljs.git"
},
"engines": {
"node": ">=18.0.0"
Expand Down
4 changes: 2 additions & 2 deletions scripts/build-vendor.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ for (const name of ['github.css', 'github-dark.css']) {
if (fs.existsSync(srcPath)) {
fs.copyFileSync(srcPath, dstPath);
} else if (!fs.existsSync(dstPath)) {
throw new Error(`[ep_syntax_highlighting] theme ${name} missing and no committed copy`);
throw new Error(`[ep_hljs] theme ${name} missing and no committed copy`);
}
}

Expand All @@ -27,7 +27,7 @@ const run = () => {
esbuild = require('esbuild');
} catch {
if (!fs.existsSync(vendorOut)) {
throw new Error('[ep_syntax_highlighting] vendor JS missing and esbuild unavailable');
throw new Error('[ep_hljs] vendor JS missing and esbuild unavailable');
}
return;
}
Expand Down
2 changes: 1 addition & 1 deletion static/css/editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
background-color: transparent !important;
}

#ep_syntax_highlighting_paused_badge {
#ep_hljs_paused_badge {
margin-left: 6px;
font-style: italic;
opacity: 0.7;
Expand Down
8 changes: 4 additions & 4 deletions static/js/domOverlay.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
'use strict';

exports.showPausedBadge = (visible) => {
const sel = document.getElementById('ep_syntax_highlighting_select');
const sel = document.getElementById('ep_hljs_select');
if (!sel) return;
let badge = document.getElementById('ep_syntax_highlighting_paused_badge');
let badge = document.getElementById('ep_hljs_paused_badge');
if (visible && !badge) {
badge = document.createElement('span');
badge.id = 'ep_syntax_highlighting_paused_badge';
badge.setAttribute('data-l10n-id', 'ep_syntax_highlighting.paused');
badge.id = 'ep_hljs_paused_badge';
badge.setAttribute('data-l10n-id', 'ep_hljs.paused');
badge.textContent = 'Highlighting paused';
sel.insertAdjacentElement('afterend', badge);
} else if (!visible && badge) {
Expand Down
26 changes: 13 additions & 13 deletions static/js/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
'use strict';

const syntaxRenderer = require('ep_syntax_highlighting/static/js/syntaxRenderer');
const codeIndent = require('ep_syntax_highlighting/static/js/codeIndent');
const themeBridge = require('ep_syntax_highlighting/static/js/themeBridge');
const syntaxRenderer = require('ep_hljs/static/js/syntaxRenderer');
const codeIndent = require('ep_hljs/static/js/codeIndent');
const themeBridge = require('ep_hljs/static/js/themeBridge');
const socketio = require('ep_etherpad-lite/static/js/socketio');
// Sub-path import keeps the client bundle clean.
const {padToggle} = require('ep_plugin_helpers/pad-toggle');
Expand All @@ -12,17 +12,17 @@ let socket = null;
let currentPadId = null;

const highlightToggle = padToggle({
pluginName: 'ep_syntax_highlighting',
pluginName: 'ep_hljs',
settingId: 'syntax-highlighting',
l10nId: 'ep_syntax_highlighting.user_enable',
l10nId: 'ep_hljs.user_enable',
defaultLabel: 'Highlight syntax in pads',
defaultEnabled: true,
});

const indentSelect = padSelect({
pluginName: 'ep_syntax_highlighting',
pluginName: 'ep_hljs',
settingId: 'indent-size',
l10nId: 'ep_syntax_highlighting.indent_size',
l10nId: 'ep_hljs.indent_size',
defaultLabel: 'Indent size',
options: [
{value: 2, label: '2 spaces'},
Expand All @@ -42,15 +42,15 @@ const loadHljs = () => {
if (typeof window !== 'undefined' && window.hljs) return Promise.resolve();
return new Promise((resolve) => {
const script = document.createElement('script');
script.src = '/static/plugins/ep_syntax_highlighting/static/js/vendor/hljs.min.js';
script.src = '/static/plugins/ep_hljs/static/js/vendor/hljs.min.js';
script.onload = resolve;
script.onerror = resolve;
document.head.appendChild(script);
});
};

const onLanguageChanged = (msg) => {
const sel = document.getElementById('ep_syntax_highlighting_select');
const sel = document.getElementById('ep_hljs_select');
if (sel) {
const newVal = msg.autoDetect ? 'auto' : msg.language;
sel.value = newVal;
Expand All @@ -63,19 +63,19 @@ const onLanguageChanged = (msg) => {
exports.postAceInit = async (hookName, context) => {
await loadHljs();
currentPadId = context.pad.getPadId();
const initial = (typeof clientVars !== 'undefined' && clientVars.ep_syntax_highlighting) ||
const initial = (typeof clientVars !== 'undefined' && clientVars.ep_hljs) ||
{language: 'auto', autoDetect: true};

const pad = require('ep_etherpad-lite/static/js/pad');
socket = socketio.connect(pad.baseURL || '/', '/syntax-highlighting');
socket.on('connect', () => socket.emit('joinPad', {padId: currentPadId}));
socket.on('languageChanged', onLanguageChanged);
socket.on('languageChangeRejected', (reason) => {
console.warn('[ep_syntax_highlighting] language change rejected:', reason && reason.error);
console.warn('[ep_hljs] language change rejected:', reason && reason.error);
});

// Reflect initial language in the dropdown without dispatching a change event.
const sel = document.getElementById('ep_syntax_highlighting_select');
const sel = document.getElementById('ep_hljs_select');
if (sel) {
sel.value = initial.autoDetect ? 'auto' : initial.language;
const $ = window.$;
Expand Down Expand Up @@ -120,5 +120,5 @@ exports.acePostWriteDomLineHTML = syntaxRenderer.acePostWriteDomLineHTML;
exports.aceKeyEvent = codeIndent.handleKey;

exports.aceEditorCSS = () => [
'ep_syntax_highlighting/static/css/editor.css',
'ep_hljs/static/css/editor.css',
];
2 changes: 1 addition & 1 deletion static/js/themeBridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

const LIGHT_FILE = 'github.css';
const DARK_FILE = 'github-dark.css';
const PATH_MARKER = '/static/plugins/ep_syntax_highlighting/static/css/themes/';
const PATH_MARKER = '/static/plugins/ep_hljs/static/css/themes/';

let listenersAttached = false;

Expand Down
6 changes: 3 additions & 3 deletions static/tests/frontend-new/specs/caret-stability.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,12 @@ test('language change clears stale token colors on inactive lines', async ({page
// Change language to a JSON parser, which will produce no tokens for
// either line (illegal JSON). The colibris skin wraps the <select> with
// niceSelect so we click that instead of the hidden native element.
const niceWrapper = page.locator('#ep_syntax_highlighting_li .nice-select');
const niceWrapper = page.locator('#ep_hljs_li .nice-select');
if (await niceWrapper.count() > 0) {
await niceWrapper.click();
await page.locator('#ep_syntax_highlighting_li .nice-select .option[data-value="json"]').click();
await page.locator('#ep_hljs_li .nice-select .option[data-value="json"]').click();
} else {
await page.locator('#ep_syntax_highlighting_select').selectOption('json');
await page.locator('#ep_hljs_select').selectOption('json');
}
await page.waitForTimeout(2500);

Expand Down
6 changes: 3 additions & 3 deletions static/tests/frontend-new/specs/code-indent.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ const setupPad = async (page: Page) => {
};

const pickJS = async (page: Page) => {
const niceWrapper = page.locator('#ep_syntax_highlighting_li .nice-select');
const niceWrapper = page.locator('#ep_hljs_li .nice-select');
if (await niceWrapper.count() > 0) {
await niceWrapper.click();
await page.locator('#ep_syntax_highlighting_li .nice-select .option[data-value="javascript"]').click();
await page.locator('#ep_hljs_li .nice-select .option[data-value="javascript"]').click();
} else {
await page.locator('#ep_syntax_highlighting_select').selectOption('javascript');
await page.locator('#ep_hljs_select').selectOption('javascript');
}
await inner(page).locator('body').click();
};
Expand Down
6 changes: 3 additions & 3 deletions static/tests/frontend-new/specs/collaboration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {goToNewPad} from 'ep_etherpad-lite/tests/frontend-new/helper/padHelper';
/** Return the current value of the underlying (hidden) native select. */
async function getSelectValue(page: import('@playwright/test').Page): Promise<string> {
return page.evaluate(() => {
const sel = document.getElementById('ep_syntax_highlighting_select') as HTMLSelectElement | null;
const sel = document.getElementById('ep_hljs_select') as HTMLSelectElement | null;
return sel ? sel.value : '';
});
}
Expand All @@ -16,7 +16,7 @@ async function waitForPad(page: import('@playwright/test').Page): Promise<void>
.frameLocator('iframe[name="ace_inner"]')
.locator('#innerdocbody[contenteditable="true"]')
.waitFor({state: 'attached', timeout: 15_000});
await page.waitForSelector('#ep_syntax_highlighting_select', {state: 'attached', timeout: 10_000});
await page.waitForSelector('#ep_hljs_select', {state: 'attached', timeout: 10_000});
// The toolbar-overlay covers the editor while the pad is initialising and
// intercepts clicks on the toolbar. Wait for it to be removed/hidden.
await page.waitForFunction(() => {
Expand Down Expand Up @@ -44,7 +44,7 @@ test('B sees the language A picked within 1s', async ({browser}) => {
// change event the plugin listens for. Avoids fighting the toolbar-overlay
// and niceSelect timing.
await a.evaluate(() => {
const sel = document.getElementById('ep_syntax_highlighting_select') as HTMLSelectElement;
const sel = document.getElementById('ep_hljs_select') as HTMLSelectElement;
sel.value = 'ruby';
const $: any = (window as any).$;
if ($ && $.fn) $(sel).trigger('change');
Expand Down
Loading
Loading