Plain Vanilla
repository·main·Indexed 20 days ago
https://github.com/jsebrech/plainvanillaA demonstration project and website built exclusively with vanilla web technologies (HTML, CSS, and JavaScript) without build tools, frameworks, or bundlers. The project includes implementations of a declarative router using the <view-route> custom element, a view transition system with startTransition() and the <view-transition> custom element, and utility functions for HTML encoding and navigation interception.
What's inside plainvanilla
- Plain Vanilla is a website and demonstration project that showcases web development using only vanilla techniques. It avoids all external tools, build steps, and frameworks, relying exclusively on the browser and standard web code (HTML, CSS, and JavaScript).
Run the Plain Vanilla project locally
mainThe project is designed to be served as a static website from the
public/directory. You can use any of the following commands to host the site locally depending on your installed environment:# Using Node.js (http-server) npx http-server public -c-1 # Using PHP php -S localhost:8000 -t public # Using Python 3 python3 -m http.server 8000 --directory publicConfigure ESLint for Plain Vanilla
mainThe project uses the ESLint flat config format (
eslint.config.cjs). The configuration extends the recommended JavaScript rules and sets up specific environment globals and ignore patterns.Key settings include:
- Globals: Includes
browserandmochaenvironments. - ECMAScript Version: Set to
2022. - Source Type: Set to
module.
Note that certain directories are explicitly ignored by the linting process to prevent errors in specific content or library paths.
/* eslint-disable no-undef */ const globals = require("globals"); const js = require("@eslint/js"); module.exports = [ js.configs.recommended, { languageOptions: { globals: { ...globals.browser, ...globals.mocha }, ecmaVersion: 2022, sourceType: "module", } }, { ignores: [ "public/blog/articles/", "**/lib/", "**/react/", ] } ];- Globals: Includes
Implement route navigation with startTransition
mainTo implement a view-transition-aware router, use
startTransitionwhenever you modify the visibility or content of route elements. This ensures that the browser's View Transition API can correctly interpolate between the old and new states of the DOM.import { startTransition } from './view-transition.js'; let currentRoute = ''; export function navigate() { currentRoute = currentRoute === 'route2' ? 'route1' : 'route2'; updateRoute1(); updateRoute2(); } function updateRoute1() { startTransition(() => { if (currentRoute === 'route1') { document.getElementById('route1').classList.add('active'); } else { document.getElementById('route1').classList.remove('active'); } }); } function updateRoute2() { startTransition(() => { const route2 = document.getElementById('route2'); if (currentRoute === 'route2') { route2.classList.add('active', 'loading'); route2.textContent = '...'; load().then((data) => startTransition(() => { route2.textContent = data; route2.classList.remove('loading'); })); } else { document.getElementById('route2').classList.remove('active'); } }); }Use `htmlRaw` to bypass HTML encoding
mainIf you have a string that is already safe or contains intentional HTML markup that should not be encoded, wrap it in
htmlRaw(). This marks the string as an instance of theHtmlclass, which tells thehtmltemplate tag to skip encoding for that specific value.import { html, htmlRaw } from './lib/html.js'; const safeMarkup = htmlRaw'<strong>Important</strong>'; const template = html`<div>${safeMarkup}</div>`; // Result: "<div><strong>Important</strong></div>"Tokenize code with tokenize()
mainThe
tokenizefunction is the low-level engine that traverses a source string and identifies tokens based on a language definition. It executes a callback for every identified token.Parameters:
src(string): The source code.lang(ShjLanguage | Array): A language identifier string (which triggers an async import) or an array ofShjLanguageComponentobjects.token(function): A callback function called for each token. Signature:(text: string, type: ShjToken) => void.
Token Types (
ShjToken):'deleted' | 'err' | 'var' | 'section' | 'kwd' | 'class' | 'cmnt' | 'insert' | 'type' | 'func' | 'bool' | 'num' | 'oper' | 'str' | 'esc'await tokenize('const x = 1;', 'js', (text, type) => { console.log(`Token: ${text}, Type: ${type}`); });Navigate programmatically with pushState()
mainThe
pushStatefunction allows you to manually trigger navigation. It updates the browser's history stack and dispatches apopstateevent to therouterEventstarget, which in turn triggers all active<view-route>elements to update their matching state.Signature:
pushState(state, unused, url)import { pushState } from './view-route.js'; // Navigate to a new URL pushState(null, null, '/new-page');Highlight a DOM element with highlightElement()
mainUse
highlightElementto transform an existing DOM element (like a<code>or<pre>tag) into a highlighted version. It automatically detects the language from the element's class name if not provided and determines the display mode based on the tag name and content length.Parameters:
elm(Element): The DOM element to highlight.lang(ShjLanguage, optional): The language identifier. If omitted, it searches for a class matchingshj-lang-{language}on the element.mode(ShjDisplayMode, optional): The display mode. If omitted, it defaults to:inlineif the tag is<code>.onelineif the text contains fewer than 2 lines.multilineotherwise.
opt(ShjOptions, optional): Customization options.
Display Modes (
ShjDisplayMode):inline: Inside a<code>element.oneline: Inside a<div>element, containing only one line.multiline: Inside a<div>element.
Note: This function modifies the element's
innerHTML,className, anddataset.lang.// Assuming <pre class="shj-lang-js">const x = 1;</pre> exists in DOM const element = document.querySelector('pre'); await highlightElement(element);Highlight text with highlightText()
mainUse
highlightTextto convert a raw code string into an HTML string with syntax highlighting applied. It supports multiline wrapping and customization via options.Parameters:
src(string): The raw code content.lang(ShjLanguage): The language identifier (e.g.,'js','css','python').multiline(boolean, default:true): If true, wraps the output in<div>elements suitable for line numbering.opt(ShjOptions, default:{}): Configuration object.
Options (
ShjOptions):hideLineNumbers(boolean): Iftrue, prevents the generation of line number placeholders.
Returns: A
Promise<string>containing the HTML content.const code = 'const x = 10;'; const highlighted = await highlightText(code, 'js'); // Or with options const highlightedWithNoLines = await highlightText(code, 'js', true, { hideLineNumbers: true });Highlight all elements with highlightAll()
mainUse
highlightAllto scan the entire document for any elements containing a class name that starts withshj-lang-and apply syntax highlighting to them automatically.// Highlights all <div class="shj-lang-ts">...</div> elements in the document await highlightAll({ hideLineNumbers: false });Use the `html` tagged template literal for auto-encoding
mainThe
htmlfunction acts as a tagged template literal that automatically encodes HTML entities in interpolated values. This prevents XSS by ensuring that characters like<,>, and&are converted to their safe HTML entity equivalents (e.g.,<,>,&).import { html } from './lib/html.js'; const userInput = '<img src=x onerror=alert(1)>'; const template = html`<div>${userInput}</div>`; // template will be: "<div><img src=x onerror=alert(1)></div>"Manage task state with TasksProvider and hooks
mainThe
TasksContextmodule provides a React context-based state management system for tasks. To use it, wrap your component tree withTasksProvider. This enables child components to access the current list of tasks viauseTasks()and trigger state updates viauseTasksDispatch().import { TasksProvider, useTasks, useTasksDispatch } from './TasksContext'; function App() { return ( <TasksProvider> <TaskList /> </TasksProvider> ); } function TaskList() { const tasks = useTasks(); const dispatch = useTasksDispatch(); // Example: adding a task const addTask = () => dispatch({ type: 'added', id: 3, text: 'New Task' }); return ( <div> {tasks.map(task => <div key={task.id}>{task.text}</div>)} <button onClick={addTask}>Add Task</button> </div> ); }