Mantle dispatches each supported language to the folding strategy that best fits its syntax. Most strategies run server- or build-side; only the toggle interaction runs in the browser.
oxc-parser walks the AST and emits folds for block/class/switch bodies, object/array literals and patterns, JSX elements and fragments, multi-line template literals, and TypeScript constructs such as interfaces, type literals, enums, modules, and mapped types.parse5 tokenizes the source with sourceCodeLocationInfo, so void elements never open folds, self-closing XML tags are honored, and nested content inside <template> folds normally. Multi-line opening tags fold their attribute list into the tag name.{ … }; JSON folds { … } and [ … ]; both skip strings and escapes, and CSS also skips comments.{ … } / [ … ] ranges fold via TextMate-scope-aware token walking; brackets inside strings, comments, or regular expressions are ignored.if … fi, case … esac, for|while|until|select … do … done, and brace-form function bodies. Same-line close markers are command-boundary aware, so variables or arguments named done, fi, or esac do not suppress valid folds.{ … }, arrays) with Ruby's keyword pairs: def, class, module, begin, do, if, case, etc., all closing on end.Plain text (text, txt, plain, plaintext) intentionally has no folds.
The examples on this page use the full server highlighter pipeline. If you're building custom highlighting with @ngrok/mantle/highlight-utils, computeFoldRanges({ language, tokens }) is a lower-level token helper for bracket, indentation, and tag folds. Use @ngrok/mantle-server-syntax-highlighter when you need the AST, raw-source, or keyword strategies listed here.
1const config = {2 name: "ngrok",3 listeners: [4 {5 addr: "localhost:8080",6 authtoken_from_env: true,7 },8 {9 addr: "localhost:9090",10 basic_auth: ["admin:secret"],11 },12 ],13 on_error: (event) => {14 console.error(`error: ${event.message}`);15 },16};1type User = {2 id: string;3 email: string;4 profile: {5 name: string;6 bio?: string;7 };8};9 10function findUser(users: User[], id: string): User | undefined {11 return users.find((user) => {12 if (user.id === id) {13 return true;14 }15 return false;16 });17}JSX elements (<Foo>…</Foo>), fragments (<>…</>), and multi-line template literals all fold in addition to function bodies and object/array literals.
1export function Profile({ user }: { user: User }) {2 const initials = useMemo(() => {3 return user.name4 .split(" ")5 .map((part) => part[0])6 .join("");7 }, [user.name]);8 9 return (10 <article11 className="profile"12 aria-labelledby="profile-name"13 data-testid="profile-card"14 >15 <header>16 <h1 id="profile-name">{user.name}</h1>17 <p>{initials}</p>18 </header>19 <Avatar20 src={user.avatarUrl}21 alt={`Avatar for ${user.name}`}22 size="lg"23 fallback={initials}24 />25 </article>26 );27}1export function Card({ title, children }) {2 return (3 <section4 className="card"5 role="region"6 aria-label={title}7 >8 <header>9 <h2>{title}</h2>10 </header>11 <div className="card-body">12 {children}13 </div>14 <img15 src="/icons/star.svg"16 alt=""17 width={16}18 height={16}19 />20 </section>21 );22}1<!doctype html>2<html lang="en">3 <head>4 <meta charset="utf-8" />5 <title>ngrok</title>6 </head>7 <body>8 <header>9 <h1>Welcome</h1>10 </header>11 <main>12 <section>13 <p>Connect your local services to the internet.</p>14 </section>15 </main>16 </body>17</html>parse5 emits startTag.startLine/endLine for multi-line opening tags, so the attribute list collapses into the tag name (matching VS Code).
1<form2 action="/submit"3 method="post"4 enctype="multipart/form-data"5 novalidate6>7 <input8 type="email"9 name="email"10 placeholder="you@example.com"11 required12 />13 <button type="submit">Send</button>14</form>XML-mode parsing routes through parse5's SVG fragment context for XML-like (foreign-content) tokenizer rules — so <empty/> and <other /> are correctly self-closing.
1<?xml version="1.0" encoding="UTF-8"?>2<project>3 <groupId>com.example</groupId>4 <artifactId>example</artifactId>5 <version>1.0.0</version>6 <dependencies>7 <dependency>8 <groupId>org.junit.jupiter</groupId>9 <artifactId>junit-jupiter</artifactId>10 <version>5.10.0</version>11 <scope>test</scope>12 </dependency>13 </dependencies>14</project>1{2 "name": "@ngrok/mantle",3 "version": "1.0.0",4 "scripts": {5 "build": "tsdown",6 "test": "vitest run",7 "lint": "oxlint"8 },9 "dependencies": {10 "react": "18.3.1",11 "react-dom": "18.3.1"12 },13 "keywords": [14 "react",15 "design-system",16 "tailwind"17 ]18}1:root {2 --color-bg: #ffffff;3 --color-fg: #111111;4}5 6.button {7 padding: 0.5rem 1rem;8 border-radius: 0.25rem;9 background: var(--color-bg);10 11 &:hover {12 background: color-mix(in srgb, var(--color-bg), black 5%);13 }14}15 16@media (prefers-color-scheme: dark) {17 :root {18 --color-bg: #0a0a0a;19 --color-fg: #f5f5f5;20 }21}1class Cache:2 def __init__(self):3 self._entries = {}4 5 def get(self, key):6 if key in self._entries:7 return self._entries[key]8 return None9 10 def put(self, key, value):11 self._entries[key] = value12 13 14def main():15 cache = Cache()16 cache.put("hello", "world")17 print(cache.get("hello"))1server:2 host: localhost3 port: 80804 tls:5 cert: /etc/ssl/server.crt6 key: /etc/ssl/server.key7clients:8 - name: web9 retries: 310 timeout: 5s11 - name: mobile12 retries: 113 timeout: 30s14features:15 streaming: true16 compression: gzip1package main2 3import (4 "fmt"5 "net/http"6)7 8type Server struct {9 Addr string10 mux *http.ServeMux11}12 13func (s *Server) Start() error {14 s.mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {15 fmt.Fprintln(w, "ok")16 })17 return http.ListenAndServe(s.Addr, s.mux)18}1package com.example;2 3import java.util.Optional;4 5public class UserService {6 private final UserRepository repository;7 8 public UserService(UserRepository repository) {9 this.repository = repository;10 }11 12 public Optional<User> findById(String id) {13 return repository.findAll().stream()14 .filter(user -> user.getId().equals(id))15 .findFirst();16 }17}1namespace Example2{3 public class UserService4 {5 private readonly IUserRepository _repository;6 7 public UserService(IUserRepository repository)8 {9 _repository = repository;10 }11 12 public User? FindById(string id)13 {14 return _repository15 .GetAll()16 .FirstOrDefault(user => user.Id == id);17 }18 }19}1use std::collections::HashMap;2 3struct Cache<V> {4 entries: HashMap<String, V>,5}6 7impl<V: Clone> Cache<V> {8 pub fn new() -> Self {9 Self {10 entries: HashMap::new(),11 }12 }13 14 pub fn get(&self, key: &str) -> Option<V> {15 self.entries.get(key).cloned()16 }17}Ruby combines bracket folding (block-style { … }, arrays) with keyword pairs — def, class, module, begin, do, if, case, etc., all closing on end.
1require "thread"2 3class HttpListener4 ALLOWED_HEADERS = [5 "X-Forwarded-For",6 "X-Forwarded-Proto",7 "X-Real-IP",8 ].freeze9 10 def initialize(addr:, **options)11 @addr = addr12 @options = {13 compression: true,14 retries: 3,15 headers: ALLOWED_HEADERS,16 }.merge(options)17 end18 19 def start(configs)20 @workers = configs.map do |config|21 Thread.new { listen(config) }22 end23 end24 25 private26 27 def listen(config)28 puts "listening on #{config.fetch(:addr)}"29 end30endif … fi, case … esac, for|while|until|select … do … done, and brace-form function bodies all fold.
1#!/usr/bin/env bash2set -euo pipefail3 4deploy() {5 if [ -z "${1:-}" ]; then6 echo "usage: deploy <env>" >&27 return 18 fi9 for region in us-east-1 eu-west-1; do10 echo "deploying to $region in $1"11 done12}13 14case "$1" in15 staging)16 deploy staging17 ;;18 prod)19 deploy prod20 ;;21esac<Foo />) get a toggle only when their attributes span multiple lines; collapsing hides the attribute lines while keeping the opening line and self-closing /> line visible.(…) — argument lists and tuple / parenthesized expressions never fold. This mirrors VS Code's default bracket folding.@import url(…) and other no-body at-rules don't have their own toggle (there's nothing to fold).; fi suppress single-line folds, but ordinary data like $done or echo done does not.