feat: update the playground from upstream reference

This commit is contained in:
maunzCache
2025-03-30 15:04:50 +02:00
committed by Michael Hoffmann
parent 3a58dc8928
commit f5ebf32b8c
6 changed files with 4318 additions and 143 deletions

View File

@@ -1,6 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<head>
<head>
<meta charset="utf-8" />
<title>Tree Sitter HCL Playground</title>
<style>
@@ -9,51 +10,71 @@
margin-left: auto;
margin-right: auto;
}
#playground-container .CodeMirror {
border: 1px solid;
}
#create-issue-btn {
padding: 0.2em;
float: right;
font-size: 1.5em;
}
#checkboxes {
padding-bottom: 1em;
}
#output-container {
border: 1px solid;
}
.highlight {
background-color: #f8f8f8;
}
</style>
</head>
<body>
</head>
<body>
<!--
This file is licensed under MIT license
Copyright (c) 2018 Max Brunsfeld
Taken from https://github.com/tree-sitter/tree-sitter/docs/section-7-playground.html
Taken from https://github.com/tree-sitter/tree-sitter/blob/master/docs/src/7-playground.md
-->
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.45.0/codemirror.min.css"
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/clusterize.js/0.18.0/clusterize.min.css"
/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/clusterize.js/0.19.0/clusterize.min.css">
<div id="playground-container">
<h1>Tree Sitter HCL Playground</h1>
<h4>Code</h4>
<div id="checkboxes">
<input id="logging-checkbox" type="checkbox" />
<div id="playground-container" class="ts-playground" style="visibility: hidden;">
<h2>Code</h2>
<div class="custom-select">
<button id="language-button" class="select-button">
<span class="selected-value">JavaScript</span>
<svg class="arrow" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
<div class="select-dropdown">
<div class="option" data-value="hcl">HCL</div>
</div>
<select id="language-select" style="display: none;">
<option value="hcl">HCL</option>
</select>
</div>
<input id="logging-checkbox" type="checkbox"></input>
<label for="logging-checkbox">Log</label>
<input id="query-checkbox" type="checkbox" />
<input id="anonymous-nodes-checkbox" type="checkbox"></input>
<label for="anonymous-nodes-checkbox">Show anonymous nodes</label>
<input id="query-checkbox" type="checkbox"></input>
<label for="query-checkbox">Query</label>
</div>
<input id="accessibility-checkbox" type="checkbox"></input>
<label for="accessibility-checkbox">Accessibility</label>
<textarea id="code-input">
example "test" {
@@ -61,12 +82,12 @@ example "test" {
}
</textarea>
<div id="query-container" style="visibility: hidden; position: absolute">
<h4>Query</h4>
<div id="query-container" style="visibility: hidden; position: absolute;">
<h2>Query</h2>
<textarea id="query-input"></textarea>
</div>
<h4>Tree</h4>
<h2>Tree</h2>
<span id="update-time"></span>
<div id="output-container-scroll">
<pre id="output-container" class="highlight"></pre>
@@ -74,10 +95,16 @@ example "test" {
<button id="create-issue-btn" type="button">Create Issue</button>
</div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.45.0/codemirror.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clusterize.js/0.18.0/clusterize.min.js"></script>
<script src="./vendor/tree-sitter.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.js"></script>
<script id="playground-script" src="./playground.js?v=3"></script>
</body>
<script type="module">
import * as TreeSitter from './vendor/web-tree-sitter.js';
window.TreeSitter = TreeSitter;
setTimeout(() => window.initializePlayground({ local: false }), 1);
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clusterize.js/0.19.0/clusterize.min.js"></script>
</body>
</html>

View File

@@ -1,49 +1,146 @@
// This file is licensed under MIT license
// Copyright (c) 2018 Max Brunsfeld
// Taken from https://github.com/tree-sitter/tree-sitter/docs/assets/playground.js
let tree;
// Taken from https://github.com/tree-sitter/tree-sitter/docs/src/assets/js/playground.js
function initializeLocalTheme() {
const themeToggle = document.getElementById('theme-toggle');
if (!themeToggle) return;
// Load saved theme or use system preference
const savedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const initialTheme = savedTheme || (prefersDark ? 'dark' : 'light');
// Set initial theme
document.documentElement.setAttribute('data-theme', initialTheme);
themeToggle.addEventListener('click', () => {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
});
}
function initializeCustomSelect({ initialValue = null, addListeners = false }) {
const button = document.getElementById('language-button');
const select = document.getElementById('language-select');
if (!button || !select) return;
const dropdown = button.nextElementSibling;
const selectedValue = button.querySelector('.selected-value');
if (initialValue) {
select.value = initialValue;
}
if (select.selectedIndex >= 0 && select.options[select.selectedIndex]) {
selectedValue.textContent = select.options[select.selectedIndex].text;
} else {
selectedValue.textContent = 'JavaScript';
}
if (addListeners) {
button.addEventListener('click', (e) => {
e.preventDefault(); // Prevent form submission
dropdown.classList.toggle('show');
});
document.addEventListener('click', (e) => {
if (!button.contains(e.target)) {
dropdown.classList.remove('show');
}
});
dropdown.querySelectorAll('.option').forEach(option => {
option.addEventListener('click', () => {
selectedValue.textContent = option.textContent;
select.value = option.dataset.value;
dropdown.classList.remove('show');
const event = new Event('change');
select.dispatchEvent(event);
});
});
}
}
window.initializePlayground = async (opts) => {
const { Parser, Language } = window.TreeSitter;
const { local } = opts;
if (local) {
initializeLocalTheme();
}
initializeCustomSelect({ addListeners: true });
let tree;
(async () => {
const CAPTURE_REGEX = /@\s*([\w\._-]+)/g;
const COLORS_BY_INDEX = [
"blue",
"chocolate",
"darkblue",
"darkcyan",
"darkgreen",
"darkred",
"darkslategray",
"dimgray",
"green",
"indigo",
"navy",
"red",
"sienna",
const LIGHT_COLORS = [
"#0550ae", // blue
"#ab5000", // rust brown
"#116329", // forest green
"#844708", // warm brown
"#6639ba", // purple
"#7d4e00", // orange brown
"#0969da", // bright blue
"#1a7f37", // green
"#cf222e", // red
"#8250df", // violet
"#6e7781", // gray
"#953800", // dark orange
"#1b7c83" // teal
];
const scriptURL = document.getElementById("playground-script").src;
const DARK_COLORS = [
"#79c0ff", // light blue
"#ffa657", // orange
"#7ee787", // light green
"#ff7b72", // salmon
"#d2a8ff", // light purple
"#ffa198", // pink
"#a5d6ff", // pale blue
"#56d364", // bright green
"#ff9492", // light red
"#e0b8ff", // pale purple
"#9ca3af", // gray
"#ffb757", // yellow orange
"#80cbc4" // light teal
];
const codeInput = document.getElementById("code-input");
const languageSelect = document.getElementById("language-select");
const loggingCheckbox = document.getElementById("logging-checkbox");
const anonymousNodes = document.getElementById('anonymous-nodes-checkbox');
const outputContainer = document.getElementById("output-container");
const createIssueBtn = document.getElementById("create-issue-btn");
const outputContainerScroll = document.getElementById(
"output-container-scroll",
);
const playgroundContainer = document.getElementById("playground-container");
const queryCheckbox = document.getElementById("query-checkbox");
const createIssueBtn = document.getElementById("create-issue-btn");
const queryContainer = document.getElementById("query-container");
const queryInput = document.getElementById("query-input");
const accessibilityCheckbox = document.getElementById("accessibility-checkbox");
const updateTimeSpan = document.getElementById("update-time");
const languagesByName = {};
loadState();
await TreeSitter.init();
await Parser.init();
const parser = new Parser();
console.log(parser, codeInput, queryInput);
const parser = new TreeSitter();
const codeEditor = CodeMirror.fromTextArea(codeInput, {
lineNumbers: true,
showCursorWhenSelecting: true,
showCursorWhenSelecting: true
});
codeEditor.on('keydown', (_, event) => {
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
event.stopPropagation(); // Prevent mdBook from going back/forward
}
});
const queryEditor = CodeMirror.fromTextArea(queryInput, {
@@ -51,6 +148,12 @@ let tree;
showCursorWhenSelecting: true,
});
queryEditor.on('keydown', (_, event) => {
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
event.stopPropagation(); // Prevent mdBook from going back/forward
}
});
const cluster = new Clusterize({
rows: [],
noDataText: null,
@@ -61,7 +164,7 @@ let tree;
const saveStateOnChange = debounce(saveState, 2000);
const runTreeQueryOnChange = debounce(runTreeQuery, 50);
let languageName = "hcl";
let languageName = languageSelect.value;
let treeRows = null;
let treeRowHighlightedIndex = -1;
let parseCount = 0;
@@ -74,20 +177,37 @@ let tree;
queryEditor.on("changes", debounce(handleQueryChange, 150));
loggingCheckbox.addEventListener("change", handleLoggingChange);
anonymousNodes.addEventListener('change', renderTree);
queryCheckbox.addEventListener("change", handleQueryEnableChange);
accessibilityCheckbox.addEventListener("change", handleQueryChange);
languageSelect.addEventListener("change", handleLanguageChange);
outputContainer.addEventListener("click", handleTreeClick);
createIssueBtn.addEventListener("click", handleCreateIssue);
handleQueryEnableChange();
await loadLanguage();
await handleLanguageChange();
playgroundContainer.style.visibility = "visible";
async function loadLanguage() {
const query = new URL(scriptURL).search;
const url = `tree-sitter-hcl.wasm${query}`;
const language = await TreeSitter.Language.load(url);
async function handleLanguageChange() {
const newLanguageName = languageSelect.value;
if (!languagesByName[newLanguageName]) {
const url = `tree-sitter-${newLanguageName}.wasm`;
languageSelect.disabled = true;
try {
languagesByName[newLanguageName] = await Language.load(url);
} catch (e) {
console.error(e);
languageSelect.value = languageName;
return;
} finally {
languageSelect.disabled = false;
}
}
tree = null;
parser.setLanguage(language);
languageName = newLanguageName;
parser.setLanguage(languagesByName[newLanguageName]);
handleCodeChange();
handleQueryChange();
}
@@ -127,7 +247,7 @@ let tree;
for (let i = 0; ; i++) {
if (i > 0 && i % 10000 === 0) {
await new Promise(r => setTimeout(r, 0));
await new Promise((r) => setTimeout(r, 0));
if (parseCount !== currentRenderCount) {
cursor.delete();
isRendering--;
@@ -137,9 +257,12 @@ let tree;
let displayName;
if (cursor.nodeIsMissing) {
displayName = `MISSING ${cursor.nodeType}`;
const nodeTypeText = cursor.nodeIsNamed ? cursor.nodeType : `"${cursor.nodeType}"`;
displayName = `MISSING ${nodeTypeText}`;
} else if (cursor.nodeIsNamed) {
displayName = cursor.nodeType;
} else if (anonymousNodes.checked) {
displayName = cursor.nodeType
}
if (visitedChildren) {
@@ -165,19 +288,25 @@ let tree;
const start = cursor.startPosition;
const end = cursor.endPosition;
const id = cursor.nodeId;
let fieldName = cursor.currentFieldName();
let fieldName = cursor.currentFieldName;
if (fieldName) {
fieldName += ": ";
} else {
fieldName = "";
}
row = `<div>${" ".repeat(
indentLevel,
)}${fieldName}<a class='plain' href="#" data-id=${id} data-range="${
start.row
},${start.column},${end.row},${end.column}">${displayName}</a> [${
start.row
}, ${start.column}] - [${end.row}, ${end.column}])`;
const nodeClass =
displayName === 'ERROR' || displayName.startsWith('MISSING')
? 'node-link error'
: cursor.nodeIsNamed
? 'node-link named'
: 'node-link anonymous';
row = `<div class="tree-row">${" ".repeat(indentLevel)}${fieldName}` +
`<a class='${nodeClass}' href="#" data-id=${id} ` +
`data-range="${start.row},${start.column},${end.row},${end.column}">` +
`${displayName}</a> <span class="position-info">` +
`[${start.row}, ${start.column}] - [${end.row}, ${end.column}]</span>`;
finishedRow = true;
}
@@ -201,6 +330,14 @@ let tree;
handleCursorMovement();
}
function getCaptureCSS(name) {
if (accessibilityCheckbox.checked) {
return `color: white; background-color: ${colorForCaptureName(name)}`;
} else {
return `color: ${colorForCaptureName(name)}`;
}
}
function runTreeQuery(_, startRow, endRow) {
if (endRow == null) {
const viewport = codeEditor.getViewport();
@@ -210,7 +347,7 @@ let tree;
codeEditor.operation(() => {
const marks = codeEditor.getAllMarks();
marks.forEach(m => m.clear());
marks.forEach((m) => m.clear());
if (tree && query) {
const captures = query.captures(
@@ -229,7 +366,7 @@ let tree;
{
inclusiveLeft: true,
inclusiveRight: true,
css: `color: ${colorForCaptureName(name)}`,
css: getCaptureCSS(name),
},
);
}
@@ -237,6 +374,21 @@ let tree;
});
}
// When we change from a dark theme to a light theme (and vice versa), the colors of the
// captures need to be updated.
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'class') {
handleQueryChange();
}
});
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
function handleQueryChange() {
if (query) {
query.delete();
@@ -245,17 +397,17 @@ let tree;
}
queryEditor.operation(() => {
queryEditor.getAllMarks().forEach(m => m.clear());
queryEditor.getAllMarks().forEach((m) => m.clear());
if (!queryCheckbox.checked) return;
const queryText = queryEditor.getValue();
try {
query = parser.getLanguage().query(queryText);
query = parser.language.query(queryText);
let match;
let row = 0;
queryEditor.eachLine(line => {
queryEditor.eachLine((line) => {
while ((match = CAPTURE_REGEX.exec(line.text))) {
queryEditor.markText(
{ line: row, ch: match.index },
@@ -322,7 +474,7 @@ let tree;
"plain",
);
}
treeRowHighlightedIndex = treeRows.findIndex(row =>
treeRowHighlightedIndex = treeRows.findIndex((row) =>
row.includes(`data-id=${node.id}`),
);
if (treeRowHighlightedIndex !== -1) {
@@ -339,9 +491,9 @@ let tree;
const containerHeight = outputContainerScroll.clientHeight;
const offset = treeRowHighlightedIndex * lineHeight;
if (scrollTop > offset - 20) {
$(outputContainerScroll).animate({ scrollTop: offset - 20 }, 150);
outputContainerScroll.animate({ scrollTop: offset - 20 }, 150);
} else if (scrollTop < offset + lineHeight + 40 - containerHeight) {
$(outputContainerScroll).animate(
outputContainerScroll.animate(
{ scrollTop: offset - containerHeight + 40 },
150,
);
@@ -364,7 +516,7 @@ ${outputText}
const queryParams = `title=${encodeURIComponent(
title,
)}&body=${encodeURIComponent(body)}`;
const url = `https://github.com/MichaHoffmann/tree-sitter-hcl/issues/new?${queryParams}`;
const url = `https://github.com/tree-sitter-grammars/tree-sitter-hcl/issues/new?${queryParams}`;
window.open(url);
}
@@ -372,7 +524,7 @@ ${outputText}
if (event.target.tagName === "A") {
event.preventDefault();
const [startRow, startColumn, endRow, endColumn] =
event.target.dataset.range.split(",").map(n => parseInt(n));
event.target.dataset.range.split(",").map((n) => parseInt(n));
codeEditor.focus();
codeEditor.setSelection(
{ line: startRow, ch: startColumn },
@@ -440,43 +592,40 @@ ${outputText}
function colorForCaptureName(capture) {
const id = query.captureNames.indexOf(capture);
return COLORS_BY_INDEX[id % COLORS_BY_INDEX.length];
}
const isDark = document.querySelector('html').classList.contains('ayu') ||
document.querySelector('html').classList.contains('coal') ||
document.querySelector('html').classList.contains('navy');
function storageGetItem(lookupKey) {
try {
return localStorage.getItem(lookupKey);
} catch {
return null;
}
}
function storageSetItem(lookupKey, value) {
try {
return localStorage.setIem(lookupKey, value);
} catch {}
const colors = isDark ? DARK_COLORS : LIGHT_COLORS;
return colors[id % colors.length];
}
function loadState() {
const language = storageGetItem("language");
const sourceCode = storageGetItem("sourceCode");
const query = storageGetItem("query");
const queryEnabled = storageGetItem("queryEnabled");
const language = localStorage.getItem("language");
const sourceCode = localStorage.getItem("sourceCode");
const anonNodes = localStorage.getItem("anonymousNodes");
const query = localStorage.getItem("query");
const queryEnabled = localStorage.getItem("queryEnabled");
if (language != null && sourceCode != null && query != null) {
queryInput.value = query;
codeInput.value = sourceCode;
languageSelect.value = language;
initializeCustomSelect({ initialValue: language });
anonymousNodes.checked = anonNodes === "true";
queryCheckbox.checked = queryEnabled === "true";
}
}
function saveState() {
storageSetItem("sourceCode", codeEditor.getValue());
localStorage.setItem("language", languageSelect.value);
localStorage.setItem("sourceCode", codeEditor.getValue());
localStorage.setItem("anonymousNodes", anonymousNodes.checked);
saveQueryState();
}
function saveQueryState() {
storageSetItem("queryEnabled", queryCheckbox.checked);
storageSetItem("query", queryEditor.getValue());
localStorage.setItem("queryEnabled", queryCheckbox.checked);
localStorage.setItem("query", queryEditor.getValue());
}
function debounce(func, wait, immediate) {
@@ -494,5 +643,4 @@ ${outputText}
if (callNow) func.apply(context, args);
};
}
})();
};

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

4001
docs/vendor/web-tree-sitter.js vendored Normal file

File diff suppressed because it is too large Load Diff