Skip to main content

Code Block Folding by Language

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.

  • AST-based (JS / TS / JSX / TSX): 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.
  • AST-based (HTML / XML): 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.
  • Raw-source (CSS / JSON): single-pass, parser-quality delimiter matchers. CSS folds { … }; JSON folds { … } and [ … ]; both skip strings and escapes, and CSS also skips comments.
  • Indentation-based (Python / YAML): blocks are detected by leading whitespace; the opener line stays visible and child lines collapse.
  • Bracket-paired (Go / Rust / Java / C# / Terraform): multi-line { … } / [ … ] ranges fold via TextMate-scope-aware token walking; brackets inside strings, comments, or regular expressions are ignored.
  • Keyword-paired (Bash / Shell): opener / closer keyword pairs from VS Code's grammars: 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.
  • Bracket + keyword (Ruby): combines bracket-paired folding (block-style { … }, 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.

← Back to Code Block

AST-based languages

JavaScript

example.js

const config = {	name: "ngrok",	listeners: [		{			addr: "localhost:8080",			authtoken_from_env: true,		},		{			addr: "localhost:9090",			basic_auth: ["admin:secret"],		},	],	on_error: (event) => {		console.error(`error: ${event.message}`);	},};

TypeScript

users.ts

type User = {	id: string;	email: string;	profile: {		name: string;		bio?: string;	};}; function findUser(users: User[], id: string): User | undefined {	return users.find((user) => {		if (user.id === id) {			return true;		}		return false;	});}

TSX

JSX elements (<Foo>…</Foo>), fragments (<>…</>), and multi-line template literals all fold in addition to function bodies and object/array literals.

profile.tsx

export function Profile({ user }: { user: User }) {	const initials = useMemo(() => {		return user.name			.split(" ")			.map((part) => part[0])			.join("");	}, [user.name]); 	return (		<article			className="profile"			aria-labelledby="profile-name"			data-testid="profile-card"		>			<header>				<h1 id="profile-name">{user.name}</h1>				<p>{initials}</p>			</header>			<Avatar				src={user.avatarUrl}				alt={`Avatar for ${user.name}`}				size="lg"				fallback={initials}			/>		</article>	);}

JSX

card.jsx

export function Card({ title, children }) {	return (		<section			className="card"			role="region"			aria-label={title}		>			<header>				<h2>{title}</h2>			</header>			<div className="card-body">				{children}			</div>			<img				src="/icons/star.svg"				alt=""				width={16}				height={16}			/>		</section>	);}

HTML

index.html

<!doctype html><html lang="en">	<head>		<meta charset="utf-8" />		<title>ngrok</title>	</head>	<body>		<header>			<h1>Welcome</h1>		</header>		<main>			<section>				<p>Connect your local services to the internet.</p>			</section>		</main>	</body></html>

Multi-line opening tag

parse5 emits startTag.startLine/endLine for multi-line opening tags, so the attribute list collapses into the tag name (matching VS Code).

form.html

<form	action="/submit"	method="post"	enctype="multipart/form-data"	novalidate>	<input		type="email"		name="email"		placeholder="you@example.com"		required	/>	<button type="submit">Send</button></form>

XML

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.

pom.xml

<?xml version="1.0" encoding="UTF-8"?><project>	<groupId>com.example</groupId>	<artifactId>example</artifactId>	<version>1.0.0</version>	<dependencies>		<dependency>			<groupId>org.junit.jupiter</groupId>			<artifactId>junit-jupiter</artifactId>			<version>5.10.0</version>			<scope>test</scope>		</dependency>	</dependencies></project>

Raw-source languages

JSON

package.json

{  "name": "@ngrok/mantle",  "version": "1.0.0",  "scripts": {    "build": "tsdown",    "test": "vitest run",    "lint": "oxlint"  },  "dependencies": {    "react": "18.3.1",    "react-dom": "18.3.1"  },  "keywords": [    "react",    "design-system",    "tailwind"  ]}

CSS

theme.css

:root {	--color-bg: #ffffff;	--color-fg: #111111;} .button {	padding: 0.5rem 1rem;	border-radius: 0.25rem;	background: var(--color-bg); 	&:hover {		background: color-mix(in srgb, var(--color-bg), black 5%);	}} @media (prefers-color-scheme: dark) {	:root {		--color-bg: #0a0a0a;		--color-fg: #f5f5f5;	}}

Indentation-based languages

Python

cache.py

class Cache:  def __init__(self):    self._entries = {}   def get(self, key):    if key in self._entries:      return self._entries[key]    return None   def put(self, key, value):    self._entries[key] = value  def main():  cache = Cache()  cache.put("hello", "world")  print(cache.get("hello"))

YAML

config.yaml

server:  host: localhost  port: 8080  tls:    cert: /etc/ssl/server.crt    key: /etc/ssl/server.keyclients:  - name: web    retries: 3    timeout: 5s  - name: mobile    retries: 1    timeout: 30sfeatures:  streaming: true  compression: gzip

Bracket-paired languages

Go

server.go

package main import (	"fmt"	"net/http") type Server struct {	Addr string	mux  *http.ServeMux} func (s *Server) Start() error {	s.mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {		fmt.Fprintln(w, "ok")	})	return http.ListenAndServe(s.Addr, s.mux)}

Java

UserService.java

package com.example; import java.util.Optional; public class UserService {	private final UserRepository repository; 	public UserService(UserRepository repository) {		this.repository = repository;	} 	public Optional<User> findById(String id) {		return repository.findAll().stream()			.filter(user -> user.getId().equals(id))			.findFirst();	}}

C#

UserService.cs

namespace Example{	public class UserService	{		private readonly IUserRepository _repository; 		public UserService(IUserRepository repository)		{			_repository = repository;		} 		public User? FindById(string id)		{			return _repository				.GetAll()				.FirstOrDefault(user => user.Id == id);		}	}}

Rust

main.rs

use std::collections::HashMap; struct Cache<V> {  entries: HashMap<String, V>,} impl<V: Clone> Cache<V> {  pub fn new() -> Self {    Self {      entries: HashMap::new(),    }  }   pub fn get(&self, key: &str) -> Option<V> {    self.entries.get(key).cloned()  }}

Terraform

HCL blocks (resource "type" "name" { … }), object values, and multi-line lists all fold as bracket pairs.

main.tf

resource "ngrok_domain" "api" {  domain      = "api.example.com"  description = "Production API"} resource "ngrok_cloud_endpoint" "api" {  url         = "https://${ngrok_domain.api.domain}"  description = "Production API endpoint"   traffic_policy = jsonencode({    on_http_request = [      {        name = "RateLimit"        actions = [{          type = "rate-limit"          config = {            name       = "per-ip"            algorithm  = "sliding_window"            capacity   = 100            rate       = "60s"            bucket_key = ["conn.client_ip"]          }        }]      },    ]  })}

Keyword-paired languages

Ruby

Ruby combines bracket folding (block-style { … }, arrays) with keyword pairs: def, class, module, begin, do, if, case, etc., all closing on end.

http_listener.rb

require "thread" class HttpListener  ALLOWED_HEADERS = [    "X-Forwarded-For",    "X-Forwarded-Proto",    "X-Real-IP",  ].freeze   def initialize(addr:, **options)    @addr = addr    @options = {      compression: true,      retries: 3,      headers: ALLOWED_HEADERS,    }.merge(options)  end   def start(configs)    @workers = configs.map do |config|      Thread.new { listen(config) }    end  end   private   def listen(config)    puts "listening on #{config.fetch(:addr)}"  endend

Bash / shell

if … fi, case … esac, for|while|until|select … do … done, and brace-form function bodies all fold.

deploy.sh

#!/usr/bin/env bashset -euo pipefail deploy() {  if [ -z "${1:-}" ]; then    echo "usage: deploy <env>" >&2    return 1  fi  for region in us-east-1 eu-west-1; do    echo "deploying to $region in $1"  done} case "$1" in  staging)    deploy staging    ;;  prod)    deploy prod    ;;esac

Caveats

  • JSX / TSX self-closing elements (<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.
  • Bracket strategy ignores (…): argument lists and tuple / parenthesized expressions never fold. This mirrors VS Code's default bracket folding.
  • Indentation strategy assumes consistent leading whitespace within a block. Mixing tabs and spaces inside the same block can produce wrong spans.
  • Single-line spans never fold: if a construct's opener and closer resolve to the same line, it emits no range. Inline objects/arrays inside a multi-line parent don't get their own toggle.
  • CSS folding is brace-only: @import url(…) and other no-body at-rules don't have their own toggle (there's nothing to fold).
  • Shell same-line close detection is conservative: line-leading closers and command-separated closers such as ; fi suppress single-line folds, but ordinary data like $done or echo done does not.