This commit is contained in:
retoor 2026-01-25 02:47:47 +01:00
parent 8c1a721c4c
commit 957e3a4762
74 changed files with 7749 additions and 292 deletions

31
apps/merge_all_wren.wren vendored Normal file
View File

@ -0,0 +1,31 @@
import "io" for File, Directory
class WrenFileReader {
static listWrenFiles(dir) {
var files = []
var entries = Directory.list(dir)
for (entry in entries) {
var fullPath = "%(dir)/%(entry)"
if (Directory.exists(fullPath)) {
files.addAll(listWrenFiles(fullPath))
} else if (entry.endsWith(".wren")) {
files.add(fullPath)
}
}
return files
}
static readFiles(files) {
for (file in files) {
System.print("--- %(file) ---")
System.print(File.read(file))
}
}
static main() {
var wrenFiles = listWrenFiles(".")
readFiles(wrenFiles)
}
}
WrenFileReader.main()

44
example/strutil_demo.wren vendored Normal file
View File

@ -0,0 +1,44 @@
// retoor <retoor@molodetz.nl>
import "strutil" for Str
import "crypto" for Hash
System.print("=== String Utility Module Demo ===\n")
System.print("--- Case Conversion ---")
System.print("toLower('HELLO World'): " + Str.toLower("HELLO World"))
System.print("toUpper('hello world'): " + Str.toUpper("hello world"))
System.print("\n--- Hex Encoding ---")
var data = "ABC"
var hex = Str.hexEncode(data)
System.print("hexEncode('ABC'): " + hex)
System.print("hexDecode('" + hex + "'): " + Str.hexDecode(hex))
System.print("\n--- SHA256 Hash with Hex ---")
var hash = Hash.sha256("hello")
System.print("SHA256('hello'): " + Hash.toHex(hash))
System.print("\n--- String Repetition ---")
System.print("repeat('ab', 5): " + Str.repeat("ab", 5))
System.print("\n--- Padding ---")
System.print("padLeft('42', 5, '0'): " + Str.padLeft("42", 5, "0"))
System.print("padRight('x', 5, '-'): " + Str.padRight("x", 5, "-"))
System.print("\n--- HTML Escaping ---")
var html = "<script>alert('xss')</script>"
System.print("Original: " + html)
System.print("Escaped: " + Str.escapeHtml(html))
System.print("\n--- JSON Escaping ---")
var text = "Line 1\nLine 2\tTabbed"
System.print("Original: Line 1\\nLine 2\\tTabbed")
System.print("Escaped: " + Str.escapeJson(text))
System.print("\n--- URL Encoding ---")
var url = "hello world & more"
var encoded = Str.urlEncode(url)
System.print("Original: " + url)
System.print("Encoded: " + encoded)
System.print("Decoded: " + Str.urlDecode(encoded))

396
manual/api/argparse.html Normal file
View File

@ -0,0 +1,396 @@
<!DOCTYPE html>
<!-- retoor <retoor@molodetz.nl> -->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>argparse - Wren-CLI Manual</title>
<link rel="stylesheet" href="../css/style.css">
</head>
<body>
<button class="mobile-menu-toggle">Menu</button>
<div class="container">
<aside class="sidebar">
<div class="sidebar-header">
<h1><a href="../index.html">Wren-CLI</a></h1>
<div class="version">v0.4.0</div>
</div>
<nav class="sidebar-nav">
<div class="section">
<span class="section-title">Getting Started</span>
<ul>
<li><a href="../getting-started/index.html">Overview</a></li>
<li><a href="../getting-started/installation.html">Installation</a></li>
<li><a href="../getting-started/first-script.html">First Script</a></li>
<li><a href="../getting-started/repl.html">Using the REPL</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">Language</span>
<ul>
<li><a href="../language/index.html">Syntax Overview</a></li>
<li><a href="../language/classes.html">Classes</a></li>
<li><a href="../language/methods.html">Methods</a></li>
<li><a href="../language/control-flow.html">Control Flow</a></li>
<li><a href="../language/fibers.html">Fibers</a></li>
<li><a href="../language/modules.html">Modules</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">API Reference</span>
<ul>
<li><a href="index.html">Overview</a></li>
<li><a href="http.html">http</a></li>
<li><a href="websocket.html">websocket</a></li>
<li><a href="tls.html">tls</a></li>
<li><a href="net.html">net</a></li>
<li><a href="dns.html">dns</a></li>
<li><a href="json.html">json</a></li>
<li><a href="base64.html">base64</a></li>
<li><a href="regex.html">regex</a></li>
<li><a href="jinja.html">jinja</a></li>
<li><a href="crypto.html">crypto</a></li>
<li><a href="os.html">os</a></li>
<li><a href="env.html">env</a></li>
<li><a href="signal.html">signal</a></li>
<li><a href="subprocess.html">subprocess</a></li>
<li><a href="sqlite.html">sqlite</a></li>
<li><a href="datetime.html">datetime</a></li>
<li><a href="timer.html">timer</a></li>
<li><a href="io.html">io</a></li>
<li><a href="scheduler.html">scheduler</a></li>
<li><a href="math.html">math</a></li>
<li><a href="uuid.html">uuid</a></li>
<li><a href="html.html">html</a></li>
<li><a href="argparse.html" class="active">argparse</a></li>
<li><a href="wdantic.html">wdantic</a></li>
<li><a href="dataset.html">dataset</a></li>
<li><a href="markdown.html">markdown</a></li>
<li><a href="web.html">web</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">Tutorials</span>
<ul>
<li><a href="../tutorials/index.html">Tutorial List</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">How-To Guides</span>
<ul>
<li><a href="../howto/index.html">How-To List</a></li>
</ul>
</div>
</nav>
</aside>
<main class="content">
<nav class="breadcrumb">
<a href="../index.html">Home</a>
<span class="separator">/</span>
<a href="index.html">API Reference</a>
<span class="separator">/</span>
<span>argparse</span>
</nav>
<article>
<h1>argparse</h1>
<p>The <code>argparse</code> module provides command-line argument parsing with support for positional arguments, optional flags, type conversion, and help generation.</p>
<pre><code>import "argparse" for ArgumentParser</code></pre>
<h2>ArgumentParser Class</h2>
<div class="class-header">
<h3>ArgumentParser</h3>
<p>Command-line argument parser</p>
</div>
<h3>Constructors</h3>
<div class="method-signature">
<span class="method-name">ArgumentParser.new</span>() → <span class="type">ArgumentParser</span>
</div>
<p>Creates a new argument parser with no description.</p>
<div class="method-signature">
<span class="method-name">ArgumentParser.new</span>(<span class="param">description</span>) → <span class="type">ArgumentParser</span>
</div>
<p>Creates a new argument parser with a description shown in help.</p>
<ul class="param-list">
<li><span class="param-name">description</span> <span class="param-type">(String)</span> - Description of the program</li>
</ul>
<h3>Properties</h3>
<div class="method-signature">
<span class="method-name">prog</span><span class="type">String</span>
</div>
<p>Gets or sets the program name shown in help output.</p>
<div class="method-signature">
<span class="method-name">description</span><span class="type">String</span>
</div>
<p>Gets or sets the program description.</p>
<h3>Instance Methods</h3>
<div class="method-signature">
<span class="method-name">addArgument</span>(<span class="param">name</span>) → <span class="type">ArgumentParser</span>
</div>
<p>Adds an argument with default options. Returns the parser for chaining.</p>
<ul class="param-list">
<li><span class="param-name">name</span> <span class="param-type">(String)</span> - Argument name (positional) or flag (starts with <code>-</code>)</li>
</ul>
<div class="method-signature">
<span class="method-name">addArgument</span>(<span class="param">name</span>, <span class="param">options</span>) → <span class="type">ArgumentParser</span>
</div>
<p>Adds an argument with specified options.</p>
<ul class="param-list">
<li><span class="param-name">name</span> <span class="param-type">(String)</span> - Argument name or flag</li>
<li><span class="param-name">options</span> <span class="param-type">(Map)</span> - Configuration options</li>
</ul>
<div class="method-signature">
<span class="method-name">parseArgs</span>() → <span class="type">Map</span>
</div>
<p>Parses arguments from <code>Process.arguments</code>.</p>
<div class="method-signature">
<span class="method-name">parseArgs</span>(<span class="param">args</span>) → <span class="type">Map</span>
</div>
<p>Parses the provided argument list.</p>
<ul class="param-list">
<li><span class="param-name">args</span> <span class="param-type">(List)</span> - List of argument strings</li>
<li><span class="returns">Returns:</span> Map of argument names to values</li>
</ul>
<div class="method-signature">
<span class="method-name">printHelp</span>()
</div>
<p>Prints formatted help information to stdout.</p>
<h2>Argument Options</h2>
<table>
<tr>
<th>Option</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td><code>long</code></td>
<td>String</td>
<td>Long form of the flag (e.g., <code>--verbose</code>)</td>
</tr>
<tr>
<td><code>type</code></td>
<td>String</td>
<td><code>"string"</code>, <code>"int"</code>, <code>"float"</code>, or <code>"bool"</code></td>
</tr>
<tr>
<td><code>default</code></td>
<td>any</td>
<td>Default value if not provided</td>
</tr>
<tr>
<td><code>required</code></td>
<td>Bool</td>
<td>Whether the argument is required</td>
</tr>
<tr>
<td><code>help</code></td>
<td>String</td>
<td>Help text description</td>
</tr>
<tr>
<td><code>choices</code></td>
<td>List</td>
<td>List of valid values</td>
</tr>
<tr>
<td><code>action</code></td>
<td>String</td>
<td>How to handle the argument</td>
</tr>
<tr>
<td><code>nargs</code></td>
<td>String/Num</td>
<td>Number of values to consume</td>
</tr>
<tr>
<td><code>dest</code></td>
<td>String</td>
<td>Name for the result map key</td>
</tr>
</table>
<h2>Actions</h2>
<table>
<tr>
<th>Action</th>
<th>Description</th>
</tr>
<tr>
<td><code>store</code></td>
<td>Store the value (default)</td>
</tr>
<tr>
<td><code>storeTrue</code></td>
<td>Store <code>true</code> when flag is present</td>
</tr>
<tr>
<td><code>storeFalse</code></td>
<td>Store <code>false</code> when flag is present</td>
</tr>
<tr>
<td><code>count</code></td>
<td>Count occurrences of the flag</td>
</tr>
<tr>
<td><code>append</code></td>
<td>Append values to a list</td>
</tr>
</table>
<h2>Nargs Values</h2>
<table>
<tr>
<th>Value</th>
<th>Description</th>
</tr>
<tr>
<td><code>*</code></td>
<td>Zero or more values (returns list)</td>
</tr>
<tr>
<td><code>+</code></td>
<td>One or more values (returns list)</td>
</tr>
<tr>
<td><code>N</code> (number)</td>
<td>Exactly N values (returns list)</td>
</tr>
</table>
<h2>Examples</h2>
<h3>Basic Usage</h3>
<pre><code>import "argparse" for ArgumentParser
var parser = ArgumentParser.new("File processing utility")
parser.prog = "myapp"
parser.addArgument("filename", {"help": "Input file to process"})
parser.addArgument("-o", {
"long": "--output",
"help": "Output file path",
"default": "output.txt"
})
var args = parser.parseArgs()
System.print("Input: %(args["filename"])")
System.print("Output: %(args["output"])")</code></pre>
<h3>Boolean Flags</h3>
<pre><code>import "argparse" for ArgumentParser
var parser = ArgumentParser.new()
parser.addArgument("-v", {
"long": "--verbose",
"action": "storeTrue",
"help": "Enable verbose output"
})
parser.addArgument("-q", {
"long": "--quiet",
"action": "storeFalse",
"dest": "verbose",
"help": "Disable verbose output"
})
var args = parser.parseArgs(["-v"])
System.print(args["verbose"]) // true</code></pre>
<h3>Type Conversion</h3>
<pre><code>import "argparse" for ArgumentParser
var parser = ArgumentParser.new()
parser.addArgument("-n", {
"long": "--count",
"type": "int",
"default": 1,
"help": "Number of iterations"
})
parser.addArgument("-t", {
"long": "--threshold",
"type": "float",
"default": 0.5,
"help": "Detection threshold"
})
var args = parser.parseArgs(["-n", "10", "-t", "0.75"])
System.print(args["count"]) // 10 (Num)
System.print(args["threshold"]) // 0.75 (Num)</code></pre>
<h3>Multiple Values</h3>
<pre><code>import "argparse" for ArgumentParser
var parser = ArgumentParser.new()
parser.addArgument("files", {
"nargs": "+",
"help": "Files to process"
})
parser.addArgument("-e", {
"long": "--exclude",
"action": "append",
"help": "Patterns to exclude"
})
var args = parser.parseArgs(["file1.txt", "file2.txt", "-e", "*.tmp", "-e", "*.bak"])
System.print(args["files"]) // ["file1.txt", "file2.txt"]
System.print(args["exclude"]) // ["*.tmp", "*.bak"]</code></pre>
<h3>Choices</h3>
<pre><code>import "argparse" for ArgumentParser
var parser = ArgumentParser.new()
parser.addArgument("-f", {
"long": "--format",
"choices": ["json", "xml", "csv"],
"default": "json",
"help": "Output format"
})
var args = parser.parseArgs(["-f", "csv"])
System.print(args["format"]) // csv</code></pre>
<h3>Verbosity Counter</h3>
<pre><code>import "argparse" for ArgumentParser
var parser = ArgumentParser.new()
parser.addArgument("-v", {
"action": "count",
"help": "Increase verbosity"
})
var args = parser.parseArgs(["-v", "-v", "-v"])
System.print(args["v"]) // 3</code></pre>
<div class="admonition note">
<div class="admonition-title">Note</div>
<p>Argument names starting with <code>-</code> are treated as optional flags. Names without a leading dash are positional arguments. Hyphens in argument names are converted to underscores in the result map.</p>
</div>
</article>
<footer class="page-footer">
<a href="html.html" class="prev">html</a>
<a href="wdantic.html" class="next">wdantic</a>
</footer>
</main>
</div>
<script src="../js/main.js"></script>
</body>
</html>

389
manual/api/dataset.html Normal file
View File

@ -0,0 +1,389 @@
<!DOCTYPE html>
<!-- retoor <retoor@molodetz.nl> -->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>dataset - Wren-CLI Manual</title>
<link rel="stylesheet" href="../css/style.css">
</head>
<body>
<button class="mobile-menu-toggle">Menu</button>
<div class="container">
<aside class="sidebar">
<div class="sidebar-header">
<h1><a href="../index.html">Wren-CLI</a></h1>
<div class="version">v0.4.0</div>
</div>
<nav class="sidebar-nav">
<div class="section">
<span class="section-title">Getting Started</span>
<ul>
<li><a href="../getting-started/index.html">Overview</a></li>
<li><a href="../getting-started/installation.html">Installation</a></li>
<li><a href="../getting-started/first-script.html">First Script</a></li>
<li><a href="../getting-started/repl.html">Using the REPL</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">Language</span>
<ul>
<li><a href="../language/index.html">Syntax Overview</a></li>
<li><a href="../language/classes.html">Classes</a></li>
<li><a href="../language/methods.html">Methods</a></li>
<li><a href="../language/control-flow.html">Control Flow</a></li>
<li><a href="../language/fibers.html">Fibers</a></li>
<li><a href="../language/modules.html">Modules</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">API Reference</span>
<ul>
<li><a href="index.html">Overview</a></li>
<li><a href="http.html">http</a></li>
<li><a href="websocket.html">websocket</a></li>
<li><a href="tls.html">tls</a></li>
<li><a href="net.html">net</a></li>
<li><a href="dns.html">dns</a></li>
<li><a href="json.html">json</a></li>
<li><a href="base64.html">base64</a></li>
<li><a href="regex.html">regex</a></li>
<li><a href="jinja.html">jinja</a></li>
<li><a href="crypto.html">crypto</a></li>
<li><a href="os.html">os</a></li>
<li><a href="env.html">env</a></li>
<li><a href="signal.html">signal</a></li>
<li><a href="subprocess.html">subprocess</a></li>
<li><a href="sqlite.html">sqlite</a></li>
<li><a href="datetime.html">datetime</a></li>
<li><a href="timer.html">timer</a></li>
<li><a href="io.html">io</a></li>
<li><a href="scheduler.html">scheduler</a></li>
<li><a href="math.html">math</a></li>
<li><a href="uuid.html">uuid</a></li>
<li><a href="html.html">html</a></li>
<li><a href="argparse.html">argparse</a></li>
<li><a href="wdantic.html">wdantic</a></li>
<li><a href="dataset.html" class="active">dataset</a></li>
<li><a href="markdown.html">markdown</a></li>
<li><a href="web.html">web</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">Tutorials</span>
<ul>
<li><a href="../tutorials/index.html">Tutorial List</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">How-To Guides</span>
<ul>
<li><a href="../howto/index.html">How-To List</a></li>
</ul>
</div>
</nav>
</aside>
<main class="content">
<nav class="breadcrumb">
<a href="../index.html">Home</a>
<span class="separator">/</span>
<a href="index.html">API Reference</a>
<span class="separator">/</span>
<span>dataset</span>
</nav>
<article>
<h1>dataset</h1>
<p>The <code>dataset</code> module provides a simple ORM-like interface for SQLite databases with automatic schema management. Tables and columns are created automatically as you insert data.</p>
<pre><code>import "dataset" for Dataset, Table</code></pre>
<h2>Dataset Class</h2>
<div class="class-header">
<h3>Dataset</h3>
<p>Database connection and table access</p>
</div>
<h3>Constructors</h3>
<div class="method-signature">
<span class="method-name">Dataset.open</span>(<span class="param">path</span>) → <span class="type">Dataset</span>
</div>
<p>Opens or creates a SQLite database file.</p>
<ul class="param-list">
<li><span class="param-name">path</span> <span class="param-type">(String)</span> - Path to the database file</li>
</ul>
<pre><code>var db = Dataset.open("data.db")</code></pre>
<div class="method-signature">
<span class="method-name">Dataset.memory</span>() → <span class="type">Dataset</span>
</div>
<p>Creates an in-memory database (data is lost when closed).</p>
<pre><code>var db = Dataset.memory()</code></pre>
<h3>Properties</h3>
<div class="method-signature">
<span class="method-name">db</span><span class="type">Database</span>
</div>
<p>Access to the underlying SQLite database for raw queries.</p>
<div class="method-signature">
<span class="method-name">tables</span><span class="type">List</span>
</div>
<p>List of table names in the database.</p>
<h3>Subscript Access</h3>
<div class="method-signature">
<span class="method-name">[tableName]</span><span class="type">Table</span>
</div>
<p>Gets a table by name. Creates the table if it does not exist on first insert.</p>
<pre><code>var users = db["users"]</code></pre>
<h3>Instance Methods</h3>
<div class="method-signature">
<span class="method-name">close</span>()
</div>
<p>Closes the database connection.</p>
<h2>Table Class</h2>
<div class="class-header">
<h3>Table</h3>
<p>CRUD operations on a database table</p>
</div>
<h3>Properties</h3>
<div class="method-signature">
<span class="method-name">name</span><span class="type">String</span>
</div>
<p>The table name.</p>
<div class="method-signature">
<span class="method-name">columns</span><span class="type">Map</span>
</div>
<p>Map of column names to SQL types.</p>
<h3>Instance Methods</h3>
<div class="method-signature">
<span class="method-name">insert</span>(<span class="param">record</span>) → <span class="type">Map</span>
</div>
<p>Inserts a record and returns it with generated <code>uid</code> and <code>created_at</code>.</p>
<ul class="param-list">
<li><span class="param-name">record</span> <span class="param-type">(Map)</span> - Data to insert</li>
<li><span class="returns">Returns:</span> Inserted record with uid and created_at</li>
</ul>
<pre><code>var user = db["users"].insert({
"name": "Alice",
"email": "alice@example.com"
})
System.print(user["uid"]) // auto-generated UUID</code></pre>
<div class="method-signature">
<span class="method-name">update</span>(<span class="param">record</span>) → <span class="type">Num</span>
</div>
<p>Updates a record by uid. Returns number of rows affected.</p>
<ul class="param-list">
<li><span class="param-name">record</span> <span class="param-type">(Map)</span> - Must contain <code>uid</code> and fields to update</li>
</ul>
<pre><code>db["users"].update({
"uid": "550e8400-e29b-41d4-a716-446655440000",
"name": "Alice Smith"
})</code></pre>
<div class="method-signature">
<span class="method-name">delete</span>(<span class="param">uid</span>) → <span class="type">Bool</span>
</div>
<p>Soft deletes a record (sets <code>deleted_at</code> timestamp). Returns true if record was deleted.</p>
<pre><code>db["users"].delete("550e8400-e29b-41d4-a716-446655440000")</code></pre>
<div class="method-signature">
<span class="method-name">hardDelete</span>(<span class="param">uid</span>) → <span class="type">Bool</span>
</div>
<p>Permanently deletes a record from the database.</p>
<div class="method-signature">
<span class="method-name">find</span>(<span class="param">conditions</span>) → <span class="type">List</span>
</div>
<p>Finds records matching conditions. Returns list of records (excludes soft-deleted).</p>
<pre><code>var admins = db["users"].find({"role": "admin"})</code></pre>
<div class="method-signature">
<span class="method-name">findOne</span>(<span class="param">conditions</span>) → <span class="type">Map|null</span>
</div>
<p>Finds first record matching conditions or null.</p>
<pre><code>var user = db["users"].findOne({"email": "alice@example.com"})</code></pre>
<div class="method-signature">
<span class="method-name">all</span>() → <span class="type">List</span>
</div>
<p>Returns all non-deleted records.</p>
<div class="method-signature">
<span class="method-name">count</span>() → <span class="type">Num</span>
</div>
<p>Returns count of non-deleted records.</p>
<h2>Query Operators</h2>
<p>Use suffixes in condition keys for comparison operators:</p>
<table>
<tr>
<th>Suffix</th>
<th>SQL Operator</th>
<th>Example</th>
</tr>
<tr>
<td><code>__gt</code></td>
<td>&gt;</td>
<td><code>{"age__gt": 18}</code></td>
</tr>
<tr>
<td><code>__lt</code></td>
<td>&lt;</td>
<td><code>{"price__lt": 100}</code></td>
</tr>
<tr>
<td><code>__gte</code></td>
<td>&gt;=</td>
<td><code>{"score__gte": 90}</code></td>
</tr>
<tr>
<td><code>__lte</code></td>
<td>&lt;=</td>
<td><code>{"score__lte": 100}</code></td>
</tr>
<tr>
<td><code>__ne</code></td>
<td>!=</td>
<td><code>{"status__ne": "deleted"}</code></td>
</tr>
<tr>
<td><code>__like</code></td>
<td>LIKE</td>
<td><code>{"name__like": "A\%"}</code></td>
</tr>
<tr>
<td><code>__in</code></td>
<td>IN</td>
<td><code>{"status__in": ["active", "pending"]}</code></td>
</tr>
<tr>
<td><code>__null</code></td>
<td>IS NULL / IS NOT NULL</td>
<td><code>{"deleted_at__null": true}</code></td>
</tr>
</table>
<h2>Automatic Features</h2>
<h3>Auto-Generated Fields</h3>
<ul>
<li><code>uid</code> - UUID v4 primary key (auto-generated if not provided)</li>
<li><code>created_at</code> - ISO timestamp (auto-generated on insert)</li>
<li><code>deleted_at</code> - ISO timestamp (set by soft delete)</li>
</ul>
<h3>Auto Schema</h3>
<ul>
<li>Tables are created automatically on first insert</li>
<li>Columns are added automatically when new fields appear</li>
<li>Type inference: Num → INTEGER/REAL, Bool → INTEGER, Map/List → TEXT (JSON)</li>
</ul>
<h3>JSON Serialization</h3>
<p>Maps and Lists are automatically serialized to JSON when stored and deserialized when retrieved.</p>
<h2>Examples</h2>
<h3>Basic CRUD</h3>
<pre><code>import "dataset" for Dataset
var db = Dataset.open("app.db")
var users = db["users"]
var user = users.insert({
"name": "Alice",
"email": "alice@example.com",
"settings": {"theme": "dark", "notifications": true}
})
System.print("Created user: %(user["uid"])")
users.update({
"uid": user["uid"],
"name": "Alice Smith"
})
var found = users.findOne({"email": "alice@example.com"})
System.print("Found: %(found["name"])")
users.delete(user["uid"])
db.close()</code></pre>
<h3>Querying with Operators</h3>
<pre><code>import "dataset" for Dataset
var db = Dataset.open("products.db")
var products = db["products"]
var expensive = products.find({"price__gt": 100})
var cheap = products.find({"price__lte": 10})
var search = products.find({"name__like": "\%phone\%"})
var featured = products.find({
"category__in": ["electronics", "gadgets"],
"stock__gt": 0
})
db.close()</code></pre>
<h3>Working with JSON Data</h3>
<pre><code>import "dataset" for Dataset
var db = Dataset.memory()
db["posts"].insert({
"title": "First Post",
"tags": ["wren", "programming", "tutorial"],
"metadata": {
"author": "Alice",
"views": 100,
"featured": true
}
})
var post = db["posts"].findOne({"title": "First Post"})
System.print(post["tags"]) // ["wren", "programming", "tutorial"]
System.print(post["metadata"]) // {"author": "Alice", ...}</code></pre>
<div class="admonition note">
<div class="admonition-title">Note</div>
<p>Soft delete is the default behavior. Records are not permanently removed but marked with a <code>deleted_at</code> timestamp. Use <code>hardDelete()</code> for permanent removal. All query methods automatically exclude soft-deleted records.</p>
</div>
<div class="admonition warning">
<div class="admonition-title">Warning</div>
<p>The <code>uid</code> field is the primary key. Do not use <code>id</code> as a field name. If you provide a <code>uid</code> during insert, it will be used instead of auto-generating one.</p>
</div>
</article>
<footer class="page-footer">
<a href="wdantic.html" class="prev">wdantic</a>
<a href="markdown.html" class="next">markdown</a>
</footer>
</main>
</div>
<script src="../js/main.js"></script>
</body>
</html>

281
manual/api/html.html Normal file
View File

@ -0,0 +1,281 @@
<!DOCTYPE html>
<!-- retoor <retoor@molodetz.nl> -->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>html - Wren-CLI Manual</title>
<link rel="stylesheet" href="../css/style.css">
</head>
<body>
<button class="mobile-menu-toggle">Menu</button>
<div class="container">
<aside class="sidebar">
<div class="sidebar-header">
<h1><a href="../index.html">Wren-CLI</a></h1>
<div class="version">v0.4.0</div>
</div>
<nav class="sidebar-nav">
<div class="section">
<span class="section-title">Getting Started</span>
<ul>
<li><a href="../getting-started/index.html">Overview</a></li>
<li><a href="../getting-started/installation.html">Installation</a></li>
<li><a href="../getting-started/first-script.html">First Script</a></li>
<li><a href="../getting-started/repl.html">Using the REPL</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">Language</span>
<ul>
<li><a href="../language/index.html">Syntax Overview</a></li>
<li><a href="../language/classes.html">Classes</a></li>
<li><a href="../language/methods.html">Methods</a></li>
<li><a href="../language/control-flow.html">Control Flow</a></li>
<li><a href="../language/fibers.html">Fibers</a></li>
<li><a href="../language/modules.html">Modules</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">API Reference</span>
<ul>
<li><a href="index.html">Overview</a></li>
<li><a href="http.html">http</a></li>
<li><a href="websocket.html">websocket</a></li>
<li><a href="tls.html">tls</a></li>
<li><a href="net.html">net</a></li>
<li><a href="dns.html">dns</a></li>
<li><a href="json.html">json</a></li>
<li><a href="base64.html">base64</a></li>
<li><a href="regex.html">regex</a></li>
<li><a href="jinja.html">jinja</a></li>
<li><a href="crypto.html">crypto</a></li>
<li><a href="os.html">os</a></li>
<li><a href="env.html">env</a></li>
<li><a href="signal.html">signal</a></li>
<li><a href="subprocess.html">subprocess</a></li>
<li><a href="sqlite.html">sqlite</a></li>
<li><a href="datetime.html">datetime</a></li>
<li><a href="timer.html">timer</a></li>
<li><a href="io.html">io</a></li>
<li><a href="scheduler.html">scheduler</a></li>
<li><a href="math.html">math</a></li>
<li><a href="uuid.html">uuid</a></li>
<li><a href="html.html" class="active">html</a></li>
<li><a href="argparse.html">argparse</a></li>
<li><a href="wdantic.html">wdantic</a></li>
<li><a href="dataset.html">dataset</a></li>
<li><a href="markdown.html">markdown</a></li>
<li><a href="web.html">web</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">Tutorials</span>
<ul>
<li><a href="../tutorials/index.html">Tutorial List</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">How-To Guides</span>
<ul>
<li><a href="../howto/index.html">How-To List</a></li>
</ul>
</div>
</nav>
</aside>
<main class="content">
<nav class="breadcrumb">
<a href="../index.html">Home</a>
<span class="separator">/</span>
<a href="index.html">API Reference</a>
<span class="separator">/</span>
<span>html</span>
</nav>
<article>
<h1>html</h1>
<p>The <code>html</code> module provides utilities for HTML and URL encoding/decoding, slug generation, and query string handling.</p>
<pre><code>import "html" for Html</code></pre>
<h2>Html Class</h2>
<div class="class-header">
<h3>Html</h3>
<p>HTML and URL encoding utilities</p>
</div>
<h3>Static Methods</h3>
<div class="method-signature">
<span class="method-name">Html.urlencode</span>(<span class="param">string</span>) → <span class="type">String</span>
</div>
<p>URL-encodes a string for use in URLs and query parameters. Spaces become <code>+</code>, special characters become percent-encoded.</p>
<ul class="param-list">
<li><span class="param-name">string</span> <span class="param-type">(String)</span> - The string to encode</li>
<li><span class="returns">Returns:</span> URL-encoded string</li>
</ul>
<pre><code>System.print(Html.urlencode("hello world")) // hello+world
System.print(Html.urlencode("a=b&c=d")) // a\%3Db\%26c\%3Dd
System.print(Html.urlencode("café")) // caf\%C3\%A9</code></pre>
<div class="method-signature">
<span class="method-name">Html.urldecode</span>(<span class="param">string</span>) → <span class="type">String</span>
</div>
<p>Decodes a URL-encoded string. Converts <code>+</code> to space and decodes percent-encoded characters.</p>
<ul class="param-list">
<li><span class="param-name">string</span> <span class="param-type">(String)</span> - The URL-encoded string</li>
<li><span class="returns">Returns:</span> Decoded string</li>
</ul>
<pre><code>System.print(Html.urldecode("hello+world")) // hello world
System.print(Html.urldecode("caf\%C3\%A9")) // café</code></pre>
<div class="method-signature">
<span class="method-name">Html.slugify</span>(<span class="param">string</span>) → <span class="type">String</span>
</div>
<p>Converts a string to a URL-friendly slug. Lowercase, alphanumeric characters with hyphens.</p>
<ul class="param-list">
<li><span class="param-name">string</span> <span class="param-type">(String)</span> - The string to slugify</li>
<li><span class="returns">Returns:</span> URL-friendly slug</li>
</ul>
<pre><code>System.print(Html.slugify("Hello World")) // hello-world
System.print(Html.slugify("My Blog Post!")) // my-blog-post
System.print(Html.slugify(" Multiple Spaces ")) // multiple-spaces</code></pre>
<div class="method-signature">
<span class="method-name">Html.quote</span>(<span class="param">string</span>) → <span class="type">String</span>
</div>
<p>Escapes HTML special characters to prevent XSS attacks.</p>
<ul class="param-list">
<li><span class="param-name">string</span> <span class="param-type">(String)</span> - The string to escape</li>
<li><span class="returns">Returns:</span> HTML-escaped string</li>
</ul>
<pre><code>System.print(Html.quote("&lt;script&gt;alert('xss')&lt;/script&gt;"))
// &amp;lt;script&amp;gt;alert(&amp;#39;xss&amp;#39;)&amp;lt;/script&amp;gt;
System.print(Html.quote("A &amp; B")) // A &amp;amp; B
System.print(Html.quote("\"quoted\"")) // &amp;quot;quoted&amp;quot;</code></pre>
<div class="method-signature">
<span class="method-name">Html.unquote</span>(<span class="param">string</span>) → <span class="type">String</span>
</div>
<p>Unescapes HTML entities back to their original characters.</p>
<ul class="param-list">
<li><span class="param-name">string</span> <span class="param-type">(String)</span> - The HTML-escaped string</li>
<li><span class="returns">Returns:</span> Unescaped string</li>
</ul>
<pre><code>System.print(Html.unquote("&amp;lt;div&amp;gt;")) // &lt;div&gt;
System.print(Html.unquote("A &amp;amp; B")) // A &amp; B</code></pre>
<div class="method-signature">
<span class="method-name">Html.encodeParams</span>(<span class="param">params</span>) → <span class="type">String</span>
</div>
<p>Encodes a map of key-value pairs into a URL query string.</p>
<ul class="param-list">
<li><span class="param-name">params</span> <span class="param-type">(Map)</span> - Map of parameters to encode</li>
<li><span class="returns">Returns:</span> URL-encoded query string</li>
</ul>
<pre><code>var params = {"name": "John Doe", "age": 30}
System.print(Html.encodeParams(params)) // name=John+Doe&amp;age=30
var search = {"q": "wren lang", "page": 1}
System.print(Html.encodeParams(search)) // q=wren+lang&amp;page=1</code></pre>
<div class="method-signature">
<span class="method-name">Html.decodeParams</span>(<span class="param">string</span>) → <span class="type">Map</span>
</div>
<p>Decodes a URL query string into a map of key-value pairs.</p>
<ul class="param-list">
<li><span class="param-name">string</span> <span class="param-type">(String)</span> - URL-encoded query string</li>
<li><span class="returns">Returns:</span> Map of decoded parameters</li>
</ul>
<pre><code>var params = Html.decodeParams("name=John+Doe&amp;age=30")
System.print(params["name"]) // John Doe
System.print(params["age"]) // 30</code></pre>
<h2>Entity Reference</h2>
<table>
<tr>
<th>Character</th>
<th>Entity</th>
</tr>
<tr>
<td>&amp;</td>
<td>&amp;amp;</td>
</tr>
<tr>
<td>&lt;</td>
<td>&amp;lt;</td>
</tr>
<tr>
<td>&gt;</td>
<td>&amp;gt;</td>
</tr>
<tr>
<td>"</td>
<td>&amp;quot;</td>
</tr>
<tr>
<td>'</td>
<td>&amp;#39;</td>
</tr>
</table>
<h2>Examples</h2>
<h3>Building URLs</h3>
<pre><code>import "html" for Html
var baseUrl = "https://api.example.com/search"
var params = {
"query": "wren programming",
"limit": 10,
"offset": 0
}
var url = baseUrl + "?" + Html.encodeParams(params)
System.print(url)
// https://api.example.com/search?query=wren+programming&amp;limit=10&amp;offset=0</code></pre>
<h3>Safe HTML Output</h3>
<pre><code>import "html" for Html
var userInput = "&lt;script&gt;alert('xss')&lt;/script&gt;"
var safeHtml = "&lt;div class=\"comment\"&gt;" + Html.quote(userInput) + "&lt;/div&gt;"
System.print(safeHtml)</code></pre>
<h3>Generating Slugs for URLs</h3>
<pre><code>import "html" for Html
var title = "How to Build Web Apps with Wren!"
var slug = Html.slugify(title)
var url = "/blog/" + slug
System.print(url) // /blog/how-to-build-web-apps-with-wren</code></pre>
<h3>Parsing Query Strings</h3>
<pre><code>import "html" for Html
var queryString = "category=books&amp;sort=price&amp;order=asc"
var params = Html.decodeParams(queryString)
for (key in params.keys) {
System.print("%(key): %(params[key])")
}</code></pre>
<div class="admonition warning">
<div class="admonition-title">Warning</div>
<p>Always use <code>Html.quote()</code> when inserting user-provided content into HTML to prevent cross-site scripting (XSS) attacks.</p>
</div>
</article>
<footer class="page-footer">
<a href="uuid.html" class="prev">uuid</a>
<a href="argparse.html" class="next">argparse</a>
</footer>
</main>
</div>
<script src="../js/main.js"></script>
</body>
</html>

View File

@ -60,6 +60,13 @@
<li><a href="io.html">io</a></li>
<li><a href="scheduler.html">scheduler</a></li>
<li><a href="math.html">math</a></li>
<li><a href="uuid.html">uuid</a></li>
<li><a href="html.html">html</a></li>
<li><a href="argparse.html">argparse</a></li>
<li><a href="wdantic.html">wdantic</a></li>
<li><a href="dataset.html">dataset</a></li>
<li><a href="markdown.html">markdown</a></li>
<li><a href="web.html">web</a></li>
</ul>
</div>
<div class="section">
@ -97,7 +104,7 @@
<article>
<h1>API Reference</h1>
<p>Wren-CLI provides 21 built-in modules covering networking, file I/O, data processing, system operations, and more. All modules are imported using the <code>import</code> statement.</p>
<p>Wren-CLI provides 28 built-in modules covering networking, file I/O, data processing, system operations, and more. All modules are imported using the <code>import</code> statement.</p>
<pre><code>import "http" for Http
import "json" for Json
@ -153,6 +160,18 @@ import "io" for File</code></pre>
<h3><a href="crypto.html">crypto</a></h3>
<p>Cryptographic hashing (MD5, SHA-1, SHA-256) and random byte generation.</p>
</div>
<div class="card">
<h3><a href="uuid.html">uuid</a></h3>
<p>UUID generation and validation with v4 support.</p>
</div>
<div class="card">
<h3><a href="html.html">html</a></h3>
<p>HTML/URL encoding, decoding, slug generation, and query string handling.</p>
</div>
<div class="card">
<h3><a href="markdown.html">markdown</a></h3>
<p>Convert Markdown text to HTML with safe mode support.</p>
</div>
</div>
<h2>System</h2>
@ -205,6 +224,28 @@ import "io" for File</code></pre>
<h3><a href="scheduler.html">scheduler</a></h3>
<p>Async fiber scheduling for non-blocking I/O operations.</p>
</div>
<div class="card">
<h3><a href="dataset.html">dataset</a></h3>
<p>Simple ORM for SQLite with automatic schema management.</p>
</div>
</div>
<h2>Application Development</h2>
<p>Modules for building web applications and command-line tools.</p>
<div class="card-grid">
<div class="card">
<h3><a href="web.html">web</a></h3>
<p>Web framework with routing, middleware, sessions, and HTTP client.</p>
</div>
<div class="card">
<h3><a href="argparse.html">argparse</a></h3>
<p>Command-line argument parsing with type conversion and help generation.</p>
</div>
<div class="card">
<h3><a href="wdantic.html">wdantic</a></h3>
<p>Data validation with schema definitions and built-in validators.</p>
</div>
</div>
<h2>Module Summary</h2>
@ -314,6 +355,41 @@ import "io" for File</code></pre>
<td>Math</td>
<td>Math functions</td>
</tr>
<tr>
<td><a href="uuid.html">uuid</a></td>
<td>Uuid</td>
<td>UUID generation</td>
</tr>
<tr>
<td><a href="html.html">html</a></td>
<td>Html</td>
<td>HTML/URL encoding</td>
</tr>
<tr>
<td><a href="argparse.html">argparse</a></td>
<td>ArgumentParser</td>
<td>CLI arguments</td>
</tr>
<tr>
<td><a href="wdantic.html">wdantic</a></td>
<td>Validator, Field, Schema, ValidationResult</td>
<td>Data validation</td>
</tr>
<tr>
<td><a href="dataset.html">dataset</a></td>
<td>Dataset, Table</td>
<td>Simple ORM</td>
</tr>
<tr>
<td><a href="markdown.html">markdown</a></td>
<td>Markdown</td>
<td>Markdown to HTML</td>
</tr>
<tr>
<td><a href="web.html">web</a></td>
<td>Application, Router, Request, Response, View, Session, Client</td>
<td>Web framework</td>
</tr>
</table>
</article>

430
manual/api/markdown.html Normal file
View File

@ -0,0 +1,430 @@
<!DOCTYPE html>
<!-- retoor <retoor@molodetz.nl> -->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>markdown - Wren-CLI Manual</title>
<link rel="stylesheet" href="../css/style.css">
</head>
<body>
<button class="mobile-menu-toggle">Menu</button>
<div class="container">
<aside class="sidebar">
<div class="sidebar-header">
<h1><a href="../index.html">Wren-CLI</a></h1>
<div class="version">v0.4.0</div>
</div>
<nav class="sidebar-nav">
<div class="section">
<span class="section-title">Getting Started</span>
<ul>
<li><a href="../getting-started/index.html">Overview</a></li>
<li><a href="../getting-started/installation.html">Installation</a></li>
<li><a href="../getting-started/first-script.html">First Script</a></li>
<li><a href="../getting-started/repl.html">Using the REPL</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">Language</span>
<ul>
<li><a href="../language/index.html">Syntax Overview</a></li>
<li><a href="../language/classes.html">Classes</a></li>
<li><a href="../language/methods.html">Methods</a></li>
<li><a href="../language/control-flow.html">Control Flow</a></li>
<li><a href="../language/fibers.html">Fibers</a></li>
<li><a href="../language/modules.html">Modules</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">API Reference</span>
<ul>
<li><a href="index.html">Overview</a></li>
<li><a href="http.html">http</a></li>
<li><a href="websocket.html">websocket</a></li>
<li><a href="tls.html">tls</a></li>
<li><a href="net.html">net</a></li>
<li><a href="dns.html">dns</a></li>
<li><a href="json.html">json</a></li>
<li><a href="base64.html">base64</a></li>
<li><a href="regex.html">regex</a></li>
<li><a href="jinja.html">jinja</a></li>
<li><a href="crypto.html">crypto</a></li>
<li><a href="os.html">os</a></li>
<li><a href="env.html">env</a></li>
<li><a href="signal.html">signal</a></li>
<li><a href="subprocess.html">subprocess</a></li>
<li><a href="sqlite.html">sqlite</a></li>
<li><a href="datetime.html">datetime</a></li>
<li><a href="timer.html">timer</a></li>
<li><a href="io.html">io</a></li>
<li><a href="scheduler.html">scheduler</a></li>
<li><a href="math.html">math</a></li>
<li><a href="uuid.html">uuid</a></li>
<li><a href="html.html">html</a></li>
<li><a href="argparse.html">argparse</a></li>
<li><a href="wdantic.html">wdantic</a></li>
<li><a href="dataset.html">dataset</a></li>
<li><a href="markdown.html" class="active">markdown</a></li>
<li><a href="web.html">web</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">Tutorials</span>
<ul>
<li><a href="../tutorials/index.html">Tutorial List</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">How-To Guides</span>
<ul>
<li><a href="../howto/index.html">How-To List</a></li>
</ul>
</div>
</nav>
</aside>
<main class="content">
<nav class="breadcrumb">
<a href="../index.html">Home</a>
<span class="separator">/</span>
<a href="index.html">API Reference</a>
<span class="separator">/</span>
<span>markdown</span>
</nav>
<article>
<h1>markdown</h1>
<p>The <code>markdown</code> module provides bidirectional conversion between Markdown and HTML. It supports common Markdown syntax including headings, emphasis, code blocks, lists, links, and images.</p>
<pre><code>import "markdown" for Markdown</code></pre>
<h2>Markdown Class</h2>
<div class="class-header">
<h3>Markdown</h3>
<p>Markdown to HTML converter</p>
</div>
<h3>Static Methods</h3>
<div class="method-signature">
<span class="method-name">Markdown.toHtml</span>(<span class="param">text</span>) → <span class="type">String</span>
</div>
<p>Converts Markdown text to HTML.</p>
<ul class="param-list">
<li><span class="param-name">text</span> <span class="param-type">(String)</span> - Markdown text</li>
<li><span class="returns">Returns:</span> HTML string</li>
</ul>
<pre><code>var html = Markdown.toHtml("# Hello World")
System.print(html) // &lt;h1&gt;Hello World&lt;/h1&gt;</code></pre>
<div class="method-signature">
<span class="method-name">Markdown.toHtml</span>(<span class="param">text</span>, <span class="param">options</span>) → <span class="type">String</span>
</div>
<p>Converts Markdown text to HTML with options.</p>
<ul class="param-list">
<li><span class="param-name">text</span> <span class="param-type">(String)</span> - Markdown text</li>
<li><span class="param-name">options</span> <span class="param-type">(Map)</span> - Conversion options</li>
<li><span class="returns">Returns:</span> HTML string</li>
</ul>
<div class="method-signature">
<span class="method-name">Markdown.fromHtml</span>(<span class="param">html</span>) → <span class="type">String</span>
</div>
<p>Converts HTML to Markdown text.</p>
<ul class="param-list">
<li><span class="param-name">html</span> <span class="param-type">(String)</span> - HTML string</li>
<li><span class="returns">Returns:</span> Markdown text</li>
</ul>
<pre><code>var md = Markdown.fromHtml("&lt;h1&gt;Hello World&lt;/h1&gt;")
System.print(md) // # Hello World</code></pre>
<div class="method-signature">
<span class="method-name">Markdown.fromHtml</span>(<span class="param">html</span>, <span class="param">options</span>) → <span class="type">String</span>
</div>
<p>Converts HTML to Markdown text with options.</p>
<ul class="param-list">
<li><span class="param-name">html</span> <span class="param-type">(String)</span> - HTML string</li>
<li><span class="param-name">options</span> <span class="param-type">(Map)</span> - Conversion options</li>
<li><span class="returns">Returns:</span> Markdown text</li>
</ul>
<h2>Options</h2>
<h3>toHtml Options</h3>
<table>
<tr>
<th>Option</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
<tr>
<td><code>safeMode</code></td>
<td>Bool</td>
<td><code>false</code></td>
<td>Escape HTML in input to prevent XSS</td>
</tr>
</table>
<h3>fromHtml Options</h3>
<table>
<tr>
<th>Option</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
<tr>
<td><code>stripUnknown</code></td>
<td>Bool</td>
<td><code>true</code></td>
<td>Strip unknown HTML tags, keeping their content</td>
</tr>
</table>
<h2>Supported Syntax</h2>
<h3>Headings</h3>
<pre><code># Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6</code></pre>
<p>Converts to <code>&lt;h1&gt;</code> through <code>&lt;h6&gt;</code> tags.</p>
<h3>Emphasis</h3>
<pre><code>*italic* or _italic_
**bold** or __bold__
~~strikethrough~~</code></pre>
<p>Converts to <code>&lt;em&gt;</code>, <code>&lt;strong&gt;</code>, and <code>&lt;del&gt;</code> tags.</p>
<h3>Code</h3>
<pre><code>Inline `code` here
```
Code block
Multiple lines
```</code></pre>
<p>Inline code uses <code>&lt;code&gt;</code>, blocks use <code>&lt;pre&gt;&lt;code&gt;</code>.</p>
<h3>Lists</h3>
<pre><code>Unordered:
- Item 1
- Item 2
* Also works
+ And this
Ordered:
1. First
2. Second
3. Third</code></pre>
<p>Creates <code>&lt;ul&gt;</code> and <code>&lt;ol&gt;</code> with <code>&lt;li&gt;</code> items.</p>
<h3>Links and Images</h3>
<pre><code>[Link text](https://example.com)
![Alt text](image.png)</code></pre>
<p>Creates <code>&lt;a href="..."&gt;</code> and <code>&lt;img src="..." alt="..."&gt;</code>.</p>
<h3>Blockquotes</h3>
<pre><code>&gt; This is a quote
&gt; Multiple lines</code></pre>
<p>Creates <code>&lt;blockquote&gt;</code> with <code>&lt;p&gt;</code> content.</p>
<h3>Horizontal Rule</h3>
<pre><code>---
***
___</code></pre>
<p>Creates <code>&lt;hr&gt;</code> tag.</p>
<h3>Paragraphs</h3>
<p>Text separated by blank lines becomes <code>&lt;p&gt;</code> elements.</p>
<h2>HTML to Markdown Conversions</h2>
<p>The <code>fromHtml</code> method converts the following HTML elements to Markdown:</p>
<table>
<tr>
<th>HTML</th>
<th>Markdown</th>
</tr>
<tr>
<td><code>&lt;h1&gt;</code> to <code>&lt;h6&gt;</code></td>
<td><code>#</code> to <code>######</code></td>
</tr>
<tr>
<td><code>&lt;strong&gt;</code>, <code>&lt;b&gt;</code></td>
<td><code>**text**</code></td>
</tr>
<tr>
<td><code>&lt;em&gt;</code>, <code>&lt;i&gt;</code></td>
<td><code>*text*</code></td>
</tr>
<tr>
<td><code>&lt;code&gt;</code></td>
<td><code>`text`</code></td>
</tr>
<tr>
<td><code>&lt;pre&gt;&lt;code&gt;</code></td>
<td>Fenced code block</td>
</tr>
<tr>
<td><code>&lt;a href="url"&gt;</code></td>
<td><code>[text](url)</code></td>
</tr>
<tr>
<td><code>&lt;img src="url" alt=""&gt;</code></td>
<td><code>![alt](url)</code></td>
</tr>
<tr>
<td><code>&lt;ul&gt;&lt;li&gt;</code></td>
<td><code>- item</code></td>
</tr>
<tr>
<td><code>&lt;ol&gt;&lt;li&gt;</code></td>
<td><code>1. item</code></td>
</tr>
<tr>
<td><code>&lt;blockquote&gt;</code></td>
<td><code>&gt; text</code></td>
</tr>
<tr>
<td><code>&lt;hr&gt;</code></td>
<td><code>---</code></td>
</tr>
<tr>
<td><code>&lt;del&gt;</code>, <code>&lt;s&gt;</code></td>
<td><code>~~text~~</code></td>
</tr>
</table>
<p>Container tags (<code>&lt;div&gt;</code>, <code>&lt;span&gt;</code>, <code>&lt;section&gt;</code>, etc.) are stripped but their content is preserved. Script and style tags are removed entirely.</p>
<h2>Examples</h2>
<h3>Basic Conversion</h3>
<pre><code>import "markdown" for Markdown
var md = "
# Welcome
This is a **Markdown** document with:
- Lists
- *Emphasis*
- `Code`
Visit [Wren](https://wren.io) for more.
"
var html = Markdown.toHtml(md)
System.print(html)</code></pre>
<h3>Safe Mode for User Content</h3>
<pre><code>import "markdown" for Markdown
var userInput = "# Title\n&lt;script&gt;alert('xss')&lt;/script&gt;\nContent here."
var safeHtml = Markdown.toHtml(userInput, {"safeMode": true})
System.print(safeHtml)</code></pre>
<h3>Rendering a Blog Post</h3>
<pre><code>import "markdown" for Markdown
import "io" for File
var postContent = File.read("post.md")
var html = Markdown.toHtml(postContent)
var page = "&lt;html&gt;
&lt;head&gt;&lt;title&gt;Blog&lt;/title&gt;&lt;/head&gt;
&lt;body&gt;
&lt;article&gt;
%(html)
&lt;/article&gt;
&lt;/body&gt;
&lt;/html&gt;"
File.write("post.html", page)</code></pre>
<h3>Code Blocks with Language Hints</h3>
<pre><code>import "markdown" for Markdown
var md = "
```wren
System.print(\"Hello, World!\")
```
"
var html = Markdown.toHtml(md)
System.print(html)
// &lt;pre&gt;&lt;code&gt;System.print("Hello, World!")&lt;/code&gt;&lt;/pre&gt;</code></pre>
<h3>Combining with Templates</h3>
<pre><code>import "markdown" for Markdown
import "jinja" for Environment, DictLoader
var env = Environment.new(DictLoader.new({
"base": "&lt;html&gt;&lt;body&gt;{{ content }}&lt;/body&gt;&lt;/html&gt;"
}))
var md = "# Hello\n\nThis is **Markdown**."
var content = Markdown.toHtml(md)
var html = env.getTemplate("base").render({"content": content})
System.print(html)</code></pre>
<h3>Converting HTML to Markdown</h3>
<pre><code>import "markdown" for Markdown
var html = "
&lt;html&gt;
&lt;body&gt;
&lt;h1&gt;Article Title&lt;/h1&gt;
&lt;p&gt;This is &lt;strong&gt;important&lt;/strong&gt; content.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First item&lt;/li&gt;
&lt;li&gt;Second item&lt;/li&gt;
&lt;/ul&gt;
&lt;/body&gt;
&lt;/html&gt;
"
var md = Markdown.fromHtml(html)
System.print(md)
// # Article Title
//
// This is **important** content.
//
// - First item
// - Second item</code></pre>
<h3>Cleaning HTML for Plain Text</h3>
<pre><code>import "markdown" for Markdown
var webContent = "&lt;div&gt;&lt;script&gt;alert('xss')&lt;/script&gt;&lt;p&gt;Safe &lt;b&gt;content&lt;/b&gt;&lt;/p&gt;&lt;/div&gt;"
var clean = Markdown.fromHtml(webContent)
System.print(clean) // Safe **content**</code></pre>
<div class="admonition warning">
<div class="admonition-title">Warning</div>
<p>When rendering user-provided Markdown, always use <code>safeMode: true</code> to prevent cross-site scripting (XSS) attacks. Safe mode escapes HTML entities in the input text.</p>
</div>
<div class="admonition note">
<div class="admonition-title">Note</div>
<p>Language identifiers after code fences (e.g., <code>```wren</code>) are currently ignored. The code is rendered without syntax highlighting. Consider using a client-side syntax highlighter like Prism.js or highlight.js for the resulting HTML.</p>
</div>
</article>
<footer class="page-footer">
<a href="dataset.html" class="prev">dataset</a>
<a href="web.html" class="next">web</a>
</footer>
</main>
</div>
<script src="../js/main.js"></script>
</body>
</html>

223
manual/api/uuid.html Normal file
View File

@ -0,0 +1,223 @@
<!DOCTYPE html>
<!-- retoor <retoor@molodetz.nl> -->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>uuid - Wren-CLI Manual</title>
<link rel="stylesheet" href="../css/style.css">
</head>
<body>
<button class="mobile-menu-toggle">Menu</button>
<div class="container">
<aside class="sidebar">
<div class="sidebar-header">
<h1><a href="../index.html">Wren-CLI</a></h1>
<div class="version">v0.4.0</div>
</div>
<nav class="sidebar-nav">
<div class="section">
<span class="section-title">Getting Started</span>
<ul>
<li><a href="../getting-started/index.html">Overview</a></li>
<li><a href="../getting-started/installation.html">Installation</a></li>
<li><a href="../getting-started/first-script.html">First Script</a></li>
<li><a href="../getting-started/repl.html">Using the REPL</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">Language</span>
<ul>
<li><a href="../language/index.html">Syntax Overview</a></li>
<li><a href="../language/classes.html">Classes</a></li>
<li><a href="../language/methods.html">Methods</a></li>
<li><a href="../language/control-flow.html">Control Flow</a></li>
<li><a href="../language/fibers.html">Fibers</a></li>
<li><a href="../language/modules.html">Modules</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">API Reference</span>
<ul>
<li><a href="index.html">Overview</a></li>
<li><a href="http.html">http</a></li>
<li><a href="websocket.html">websocket</a></li>
<li><a href="tls.html">tls</a></li>
<li><a href="net.html">net</a></li>
<li><a href="dns.html">dns</a></li>
<li><a href="json.html">json</a></li>
<li><a href="base64.html">base64</a></li>
<li><a href="regex.html">regex</a></li>
<li><a href="jinja.html">jinja</a></li>
<li><a href="crypto.html">crypto</a></li>
<li><a href="os.html">os</a></li>
<li><a href="env.html">env</a></li>
<li><a href="signal.html">signal</a></li>
<li><a href="subprocess.html">subprocess</a></li>
<li><a href="sqlite.html">sqlite</a></li>
<li><a href="datetime.html">datetime</a></li>
<li><a href="timer.html">timer</a></li>
<li><a href="io.html">io</a></li>
<li><a href="scheduler.html">scheduler</a></li>
<li><a href="math.html">math</a></li>
<li><a href="uuid.html" class="active">uuid</a></li>
<li><a href="html.html">html</a></li>
<li><a href="argparse.html">argparse</a></li>
<li><a href="wdantic.html">wdantic</a></li>
<li><a href="dataset.html">dataset</a></li>
<li><a href="markdown.html">markdown</a></li>
<li><a href="web.html">web</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">Tutorials</span>
<ul>
<li><a href="../tutorials/index.html">Tutorial List</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">How-To Guides</span>
<ul>
<li><a href="../howto/index.html">How-To List</a></li>
</ul>
</div>
</nav>
</aside>
<main class="content">
<nav class="breadcrumb">
<a href="../index.html">Home</a>
<span class="separator">/</span>
<a href="index.html">API Reference</a>
<span class="separator">/</span>
<span>uuid</span>
</nav>
<article>
<h1>uuid</h1>
<p>The <code>uuid</code> module provides UUID (Universally Unique Identifier) generation and validation. It supports version 4 UUIDs which are randomly generated.</p>
<pre><code>import "uuid" for Uuid</code></pre>
<h2>Uuid Class</h2>
<div class="class-header">
<h3>Uuid</h3>
<p>UUID generation and validation</p>
</div>
<h3>Static Methods</h3>
<div class="method-signature">
<span class="method-name">Uuid.v4</span>() → <span class="type">String</span>
</div>
<p>Generates a new random version 4 UUID.</p>
<ul class="param-list">
<li><span class="returns">Returns:</span> A 36-character UUID string in the format <code>xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx</code></li>
</ul>
<pre><code>var id = Uuid.v4()
System.print(id) // e.g., "550e8400-e29b-41d4-a716-446655440000"</code></pre>
<div class="method-signature">
<span class="method-name">Uuid.isValid</span>(<span class="param">string</span>) → <span class="type">Bool</span>
</div>
<p>Checks if a string is a valid UUID format (any version).</p>
<ul class="param-list">
<li><span class="param-name">string</span> <span class="param-type">(String)</span> - The string to validate</li>
<li><span class="returns">Returns:</span> <code>true</code> if the string is a valid UUID format, <code>false</code> otherwise</li>
</ul>
<pre><code>System.print(Uuid.isValid("550e8400-e29b-41d4-a716-446655440000")) // true
System.print(Uuid.isValid("not-a-uuid")) // false
System.print(Uuid.isValid("550e8400e29b41d4a716446655440000")) // false (missing hyphens)</code></pre>
<div class="method-signature">
<span class="method-name">Uuid.isV4</span>(<span class="param">string</span>) → <span class="type">Bool</span>
</div>
<p>Checks if a string is a valid version 4 UUID.</p>
<ul class="param-list">
<li><span class="param-name">string</span> <span class="param-type">(String)</span> - The string to validate</li>
<li><span class="returns">Returns:</span> <code>true</code> if the string is a valid v4 UUID, <code>false</code> otherwise</li>
</ul>
<pre><code>var id = Uuid.v4()
System.print(Uuid.isV4(id)) // true
System.print(Uuid.isV4("550e8400-e29b-11d4-a716-446655440000")) // false (version 1)</code></pre>
<h2>UUID Format</h2>
<p>UUIDs are 128-bit identifiers represented as 32 hexadecimal characters separated by hyphens into five groups:</p>
<pre><code>xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
8 4 4 4 12 = 32 hex chars + 4 hyphens = 36 chars</code></pre>
<table>
<tr>
<th>Position</th>
<th>Description</th>
</tr>
<tr>
<td>M</td>
<td>Version number (4 for v4 UUIDs)</td>
</tr>
<tr>
<td>N</td>
<td>Variant (8, 9, a, or b for RFC 4122 UUIDs)</td>
</tr>
</table>
<h2>Examples</h2>
<h3>Generating Unique Identifiers</h3>
<pre><code>import "uuid" for Uuid
var userId = Uuid.v4()
var sessionId = Uuid.v4()
System.print("User ID: %(userId)")
System.print("Session ID: %(sessionId)")</code></pre>
<h3>Validating User Input</h3>
<pre><code>import "uuid" for Uuid
var input = "550e8400-e29b-41d4-a716-446655440000"
if (Uuid.isValid(input)) {
System.print("Valid UUID")
if (Uuid.isV4(input)) {
System.print("This is a v4 UUID")
}
} else {
System.print("Invalid UUID format")
}</code></pre>
<h3>Using with Database Records</h3>
<pre><code>import "uuid" for Uuid
class User {
construct new(name) {
_id = Uuid.v4()
_name = name
}
id { _id }
name { _name }
}
var user = User.new("Alice")
System.print("Created user %(user.name) with ID %(user.id)")</code></pre>
<div class="admonition note">
<div class="admonition-title">Note</div>
<p>Version 4 UUIDs are generated using cryptographically secure random bytes from the <code>crypto</code> module. The probability of generating duplicate UUIDs is astronomically low.</p>
</div>
</article>
<footer class="page-footer">
<a href="math.html" class="prev">math</a>
<a href="html.html" class="next">html</a>
</footer>
</main>
</div>
<script src="../js/main.js"></script>
</body>
</html>

410
manual/api/wdantic.html Normal file
View File

@ -0,0 +1,410 @@
<!DOCTYPE html>
<!-- retoor <retoor@molodetz.nl> -->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>wdantic - Wren-CLI Manual</title>
<link rel="stylesheet" href="../css/style.css">
</head>
<body>
<button class="mobile-menu-toggle">Menu</button>
<div class="container">
<aside class="sidebar">
<div class="sidebar-header">
<h1><a href="../index.html">Wren-CLI</a></h1>
<div class="version">v0.4.0</div>
</div>
<nav class="sidebar-nav">
<div class="section">
<span class="section-title">Getting Started</span>
<ul>
<li><a href="../getting-started/index.html">Overview</a></li>
<li><a href="../getting-started/installation.html">Installation</a></li>
<li><a href="../getting-started/first-script.html">First Script</a></li>
<li><a href="../getting-started/repl.html">Using the REPL</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">Language</span>
<ul>
<li><a href="../language/index.html">Syntax Overview</a></li>
<li><a href="../language/classes.html">Classes</a></li>
<li><a href="../language/methods.html">Methods</a></li>
<li><a href="../language/control-flow.html">Control Flow</a></li>
<li><a href="../language/fibers.html">Fibers</a></li>
<li><a href="../language/modules.html">Modules</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">API Reference</span>
<ul>
<li><a href="index.html">Overview</a></li>
<li><a href="http.html">http</a></li>
<li><a href="websocket.html">websocket</a></li>
<li><a href="tls.html">tls</a></li>
<li><a href="net.html">net</a></li>
<li><a href="dns.html">dns</a></li>
<li><a href="json.html">json</a></li>
<li><a href="base64.html">base64</a></li>
<li><a href="regex.html">regex</a></li>
<li><a href="jinja.html">jinja</a></li>
<li><a href="crypto.html">crypto</a></li>
<li><a href="os.html">os</a></li>
<li><a href="env.html">env</a></li>
<li><a href="signal.html">signal</a></li>
<li><a href="subprocess.html">subprocess</a></li>
<li><a href="sqlite.html">sqlite</a></li>
<li><a href="datetime.html">datetime</a></li>
<li><a href="timer.html">timer</a></li>
<li><a href="io.html">io</a></li>
<li><a href="scheduler.html">scheduler</a></li>
<li><a href="math.html">math</a></li>
<li><a href="uuid.html">uuid</a></li>
<li><a href="html.html">html</a></li>
<li><a href="argparse.html">argparse</a></li>
<li><a href="wdantic.html" class="active">wdantic</a></li>
<li><a href="dataset.html">dataset</a></li>
<li><a href="markdown.html">markdown</a></li>
<li><a href="web.html">web</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">Tutorials</span>
<ul>
<li><a href="../tutorials/index.html">Tutorial List</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">How-To Guides</span>
<ul>
<li><a href="../howto/index.html">How-To List</a></li>
</ul>
</div>
</nav>
</aside>
<main class="content">
<nav class="breadcrumb">
<a href="../index.html">Home</a>
<span class="separator">/</span>
<a href="index.html">API Reference</a>
<span class="separator">/</span>
<span>wdantic</span>
</nav>
<article>
<h1>wdantic</h1>
<p>The <code>wdantic</code> module provides data validation similar to Python's Pydantic. It includes standalone validators and a schema-based validation system for structured data.</p>
<pre><code>import "wdantic" for Validator, Field, Schema, ValidationResult</code></pre>
<h2>Validator Class</h2>
<div class="class-header">
<h3>Validator</h3>
<p>Standalone validation functions</p>
</div>
<h3>Static Methods</h3>
<div class="method-signature">
<span class="method-name">Validator.email</span>(<span class="param">value</span>) → <span class="type">Bool</span>
</div>
<p>Validates email address format.</p>
<pre><code>Validator.email("user@example.com") // true
Validator.email("invalid") // false</code></pre>
<div class="method-signature">
<span class="method-name">Validator.domain</span>(<span class="param">value</span>) → <span class="type">Bool</span>
</div>
<p>Validates domain name format.</p>
<pre><code>Validator.domain("example.com") // true
Validator.domain("localhost") // false</code></pre>
<div class="method-signature">
<span class="method-name">Validator.url</span>(<span class="param">value</span>) → <span class="type">Bool</span>
</div>
<p>Validates HTTP/HTTPS URL format.</p>
<pre><code>Validator.url("https://example.com/path") // true
Validator.url("ftp://example.com") // false</code></pre>
<div class="method-signature">
<span class="method-name">Validator.uuid</span>(<span class="param">value</span>) → <span class="type">Bool</span>
</div>
<p>Validates UUID format.</p>
<pre><code>Validator.uuid("550e8400-e29b-41d4-a716-446655440000") // true</code></pre>
<div class="method-signature">
<span class="method-name">Validator.safeStr</span>(<span class="param">value</span>) → <span class="type">Bool</span>
</div>
<p>Checks if string contains only printable ASCII characters (32-126).</p>
<pre><code>Validator.safeStr("Hello World") // true
Validator.safeStr("Hello\x00") // false</code></pre>
<div class="method-signature">
<span class="method-name">Validator.base64</span>(<span class="param">value</span>) → <span class="type">Bool</span>
</div>
<p>Validates Base64 encoding format.</p>
<pre><code>Validator.base64("SGVsbG8=") // true
Validator.base64("invalid!") // false</code></pre>
<div class="method-signature">
<span class="method-name">Validator.json</span>(<span class="param">value</span>) → <span class="type">Bool</span>
</div>
<p>Checks if string is valid JSON.</p>
<pre><code>Validator.json("{\"key\": \"value\"}") // true
Validator.json("{invalid}") // false</code></pre>
<div class="method-signature">
<span class="method-name">Validator.ipv4</span>(<span class="param">value</span>) → <span class="type">Bool</span>
</div>
<p>Validates IPv4 address format.</p>
<pre><code>Validator.ipv4("192.168.1.1") // true
Validator.ipv4("256.0.0.1") // false</code></pre>
<div class="method-signature">
<span class="method-name">Validator.minLength</span>(<span class="param">value</span>, <span class="param">min</span>) → <span class="type">Bool</span>
</div>
<p>Checks minimum length of string or list.</p>
<div class="method-signature">
<span class="method-name">Validator.maxLength</span>(<span class="param">value</span>, <span class="param">max</span>) → <span class="type">Bool</span>
</div>
<p>Checks maximum length of string or list.</p>
<div class="method-signature">
<span class="method-name">Validator.range</span>(<span class="param">value</span>, <span class="param">min</span>, <span class="param">max</span>) → <span class="type">Bool</span>
</div>
<p>Checks if number is within range (inclusive).</p>
<pre><code>Validator.range(5, 1, 10) // true
Validator.range(15, 1, 10) // false</code></pre>
<div class="method-signature">
<span class="method-name">Validator.positive</span>(<span class="param">value</span>) → <span class="type">Bool</span>
</div>
<p>Checks if number is positive (greater than 0).</p>
<div class="method-signature">
<span class="method-name">Validator.negative</span>(<span class="param">value</span>) → <span class="type">Bool</span>
</div>
<p>Checks if number is negative (less than 0).</p>
<div class="method-signature">
<span class="method-name">Validator.integer</span>(<span class="param">value</span>) → <span class="type">Bool</span>
</div>
<p>Checks if number is an integer (no decimal part).</p>
<div class="method-signature">
<span class="method-name">Validator.regex</span>(<span class="param">value</span>, <span class="param">pattern</span>) → <span class="type">Bool</span>
</div>
<p>Tests if value matches a regular expression pattern.</p>
<pre><code>Validator.regex("abc123", "^[a-z]+[0-9]+$") // true</code></pre>
<h2>Field Class</h2>
<div class="class-header">
<h3>Field</h3>
<p>Factory for creating field definitions</p>
</div>
<h3>Static Methods</h3>
<div class="method-signature">
<span class="method-name">Field.string</span>() → <span class="type">StringField</span>
</div>
<div class="method-signature">
<span class="method-name">Field.string</span>(<span class="param">options</span>) → <span class="type">StringField</span>
</div>
<p>Creates a string field. Options: <code>minLength</code>, <code>maxLength</code>, <code>pattern</code>, <code>required</code>, <code>default</code>.</p>
<div class="method-signature">
<span class="method-name">Field.integer</span>() → <span class="type">IntegerField</span>
</div>
<div class="method-signature">
<span class="method-name">Field.integer</span>(<span class="param">options</span>) → <span class="type">IntegerField</span>
</div>
<p>Creates an integer field. Options: <code>min</code>, <code>max</code>, <code>required</code>, <code>default</code>.</p>
<div class="method-signature">
<span class="method-name">Field.number</span>() → <span class="type">NumberField</span>
</div>
<div class="method-signature">
<span class="method-name">Field.number</span>(<span class="param">options</span>) → <span class="type">NumberField</span>
</div>
<p>Creates a number field (integer or float). Options: <code>min</code>, <code>max</code>, <code>required</code>, <code>default</code>.</p>
<div class="method-signature">
<span class="method-name">Field.email</span>() → <span class="type">EmailField</span>
</div>
<p>Creates a field that validates email format.</p>
<div class="method-signature">
<span class="method-name">Field.boolean</span>() → <span class="type">BooleanField</span>
</div>
<p>Creates a boolean field.</p>
<div class="method-signature">
<span class="method-name">Field.list</span>(<span class="param">itemType</span>) → <span class="type">ListField</span>
</div>
<p>Creates a list field with typed items.</p>
<div class="method-signature">
<span class="method-name">Field.map</span>() → <span class="type">MapField</span>
</div>
<p>Creates a map/object field.</p>
<div class="method-signature">
<span class="method-name">Field.optional</span>(<span class="param">fieldDef</span>) → <span class="type">OptionalField</span>
</div>
<p>Makes any field optional (not required).</p>
<h2>Schema Class</h2>
<div class="class-header">
<h3>Schema</h3>
<p>Validates data against a field definition map</p>
</div>
<h3>Constructor</h3>
<div class="method-signature">
<span class="method-name">Schema.new</span>(<span class="param">definition</span>) → <span class="type">Schema</span>
</div>
<p>Creates a schema from a map of field names to field definitions.</p>
<h3>Instance Methods</h3>
<div class="method-signature">
<span class="method-name">validate</span>(<span class="param">data</span>) → <span class="type">ValidationResult</span>
</div>
<p>Validates data against the schema.</p>
<div class="method-signature">
<span class="method-name">validateOrAbort</span>(<span class="param">data</span>) → <span class="type">Map</span>
</div>
<p>Validates data and aborts on failure. Returns validated data on success.</p>
<h2>ValidationResult Class</h2>
<div class="class-header">
<h3>ValidationResult</h3>
<p>Result of schema validation</p>
</div>
<h3>Properties</h3>
<div class="method-signature">
<span class="method-name">isValid</span><span class="type">Bool</span>
</div>
<p>Whether validation passed.</p>
<div class="method-signature">
<span class="method-name">errors</span><span class="type">List</span>
</div>
<p>List of ValidationError objects.</p>
<div class="method-signature">
<span class="method-name">data</span><span class="type">Map</span>
</div>
<p>Validated and coerced data (null if invalid).</p>
<h2>Examples</h2>
<h3>Basic Schema Validation</h3>
<pre><code>import "wdantic" for Field, Schema
var userSchema = Schema.new({
"name": Field.string({"minLength": 1, "maxLength": 100}),
"email": Field.email(),
"age": Field.integer({"min": 0, "max": 150})
})
var result = userSchema.validate({
"name": "Alice",
"email": "alice@example.com",
"age": 30
})
if (result.isValid) {
System.print("Valid: %(result.data)")
} else {
for (error in result.errors) {
System.print("Error: %(error)")
}
}</code></pre>
<h3>Optional Fields</h3>
<pre><code>import "wdantic" for Field, Schema
var schema = Schema.new({
"title": Field.string(),
"description": Field.optional(Field.string()),
"tags": Field.optional(Field.list(Field.string()))
})
var result = schema.validate({
"title": "My Post"
})
System.print(result.isValid) // true</code></pre>
<h3>Nested Lists</h3>
<pre><code>import "wdantic" for Field, Schema
var schema = Schema.new({
"numbers": Field.list(Field.integer({"min": 0}))
})
var result = schema.validate({
"numbers": [1, 2, 3, -1] // -1 will fail
})
System.print(result.isValid) // false</code></pre>
<h3>Using validateOrAbort</h3>
<pre><code>import "wdantic" for Field, Schema
var schema = Schema.new({
"username": Field.string({"minLength": 3}),
"password": Field.string({"minLength": 8})
})
var data = schema.validateOrAbort({
"username": "admin",
"password": "secret123"
})</code></pre>
<h3>Standalone Validators</h3>
<pre><code>import "wdantic" for Validator
var email = "user@example.com"
if (Validator.email(email)) {
System.print("Valid email")
}
var ip = "192.168.1.1"
if (Validator.ipv4(ip)) {
System.print("Valid IP address")
}
var jsonStr = "{\"key\": 123}"
if (Validator.json(jsonStr)) {
System.print("Valid JSON")
}</code></pre>
<div class="admonition note">
<div class="admonition-title">Note</div>
<p>Field types automatically coerce values when possible. For example, <code>IntegerField</code> will convert the string <code>"42"</code> to the number <code>42</code>.</p>
</div>
</article>
<footer class="page-footer">
<a href="argparse.html" class="prev">argparse</a>
<a href="dataset.html" class="next">dataset</a>
</footer>
</main>
</div>
<script src="../js/main.js"></script>
</body>
</html>

550
manual/api/web.html Normal file
View File

@ -0,0 +1,550 @@
<!DOCTYPE html>
<!-- retoor <retoor@molodetz.nl> -->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>web - Wren-CLI Manual</title>
<link rel="stylesheet" href="../css/style.css">
</head>
<body>
<button class="mobile-menu-toggle">Menu</button>
<div class="container">
<aside class="sidebar">
<div class="sidebar-header">
<h1><a href="../index.html">Wren-CLI</a></h1>
<div class="version">v0.4.0</div>
</div>
<nav class="sidebar-nav">
<div class="section">
<span class="section-title">Getting Started</span>
<ul>
<li><a href="../getting-started/index.html">Overview</a></li>
<li><a href="../getting-started/installation.html">Installation</a></li>
<li><a href="../getting-started/first-script.html">First Script</a></li>
<li><a href="../getting-started/repl.html">Using the REPL</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">Language</span>
<ul>
<li><a href="../language/index.html">Syntax Overview</a></li>
<li><a href="../language/classes.html">Classes</a></li>
<li><a href="../language/methods.html">Methods</a></li>
<li><a href="../language/control-flow.html">Control Flow</a></li>
<li><a href="../language/fibers.html">Fibers</a></li>
<li><a href="../language/modules.html">Modules</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">API Reference</span>
<ul>
<li><a href="index.html">Overview</a></li>
<li><a href="http.html">http</a></li>
<li><a href="websocket.html">websocket</a></li>
<li><a href="tls.html">tls</a></li>
<li><a href="net.html">net</a></li>
<li><a href="dns.html">dns</a></li>
<li><a href="json.html">json</a></li>
<li><a href="base64.html">base64</a></li>
<li><a href="regex.html">regex</a></li>
<li><a href="jinja.html">jinja</a></li>
<li><a href="crypto.html">crypto</a></li>
<li><a href="os.html">os</a></li>
<li><a href="env.html">env</a></li>
<li><a href="signal.html">signal</a></li>
<li><a href="subprocess.html">subprocess</a></li>
<li><a href="sqlite.html">sqlite</a></li>
<li><a href="datetime.html">datetime</a></li>
<li><a href="timer.html">timer</a></li>
<li><a href="io.html">io</a></li>
<li><a href="scheduler.html">scheduler</a></li>
<li><a href="math.html">math</a></li>
<li><a href="uuid.html">uuid</a></li>
<li><a href="html.html">html</a></li>
<li><a href="argparse.html">argparse</a></li>
<li><a href="wdantic.html">wdantic</a></li>
<li><a href="dataset.html">dataset</a></li>
<li><a href="markdown.html">markdown</a></li>
<li><a href="web.html" class="active">web</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">Tutorials</span>
<ul>
<li><a href="../tutorials/index.html">Tutorial List</a></li>
</ul>
</div>
<div class="section">
<span class="section-title">How-To Guides</span>
<ul>
<li><a href="../howto/index.html">How-To List</a></li>
</ul>
</div>
</nav>
</aside>
<main class="content">
<nav class="breadcrumb">
<a href="../index.html">Home</a>
<span class="separator">/</span>
<a href="index.html">API Reference</a>
<span class="separator">/</span>
<span>web</span>
</nav>
<article>
<h1>web</h1>
<p>The <code>web</code> module provides a web framework for building HTTP servers and a client for making HTTP requests. It includes routing, middleware, sessions, static file serving, and class-based views.</p>
<pre><code>import "web" for Application, Router, Request, Response, View, Session, Client</code></pre>
<h2>Application Class</h2>
<div class="class-header">
<h3>Application</h3>
<p>HTTP server with routing and middleware</p>
</div>
<h3>Constructor</h3>
<div class="method-signature">
<span class="method-name">Application.new</span>() → <span class="type">Application</span>
</div>
<p>Creates a new web application.</p>
<h3>Properties</h3>
<div class="method-signature">
<span class="method-name">router</span><span class="type">Router</span>
</div>
<p>Access to the underlying router.</p>
<div class="method-signature">
<span class="method-name">sessionStore</span><span class="type">SessionStore</span>
</div>
<p>Access to the session storage.</p>
<h3>Routing Methods</h3>
<div class="method-signature">
<span class="method-name">get</span>(<span class="param">path</span>, <span class="param">handler</span>) → <span class="type">Application</span>
</div>
<div class="method-signature">
<span class="method-name">post</span>(<span class="param">path</span>, <span class="param">handler</span>) → <span class="type">Application</span>
</div>
<div class="method-signature">
<span class="method-name">put</span>(<span class="param">path</span>, <span class="param">handler</span>) → <span class="type">Application</span>
</div>
<div class="method-signature">
<span class="method-name">delete</span>(<span class="param">path</span>, <span class="param">handler</span>) → <span class="type">Application</span>
</div>
<div class="method-signature">
<span class="method-name">patch</span>(<span class="param">path</span>, <span class="param">handler</span>) → <span class="type">Application</span>
</div>
<p>Register route handlers. Handler receives <code>Request</code> and returns <code>Response</code>.</p>
<ul class="param-list">
<li><span class="param-name">path</span> <span class="param-type">(String)</span> - URL path with optional parameters (<code>:param</code>) or wildcards (<code>*</code>)</li>
<li><span class="param-name">handler</span> <span class="param-type">(Fn)</span> - Function receiving Request, returning Response</li>
</ul>
<div class="method-signature">
<span class="method-name">addView</span>(<span class="param">path</span>, <span class="param">viewClass</span>) → <span class="type">Application</span>
</div>
<p>Registers a class-based view for all HTTP methods.</p>
<div class="method-signature">
<span class="method-name">static_</span>(<span class="param">prefix</span>, <span class="param">directory</span>) → <span class="type">Application</span>
</div>
<p>Serves static files from a directory.</p>
<pre><code>app.static_("/assets", "./public")</code></pre>
<div class="method-signature">
<span class="method-name">use</span>(<span class="param">middleware</span>) → <span class="type">Application</span>
</div>
<p>Adds middleware function. Middleware receives Request, returns Response or null to continue.</p>
<div class="method-signature">
<span class="method-name">run</span>(<span class="param">host</span>, <span class="param">port</span>)
</div>
<p>Starts the server (blocking).</p>
<h2>Request Class</h2>
<div class="class-header">
<h3>Request</h3>
<p>Incoming HTTP request</p>
</div>
<h3>Properties</h3>
<table>
<tr>
<th>Property</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td><code>method</code></td>
<td>String</td>
<td>HTTP method (GET, POST, etc.)</td>
</tr>
<tr>
<td><code>path</code></td>
<td>String</td>
<td>Request path without query string</td>
</tr>
<tr>
<td><code>query</code></td>
<td>Map</td>
<td>Parsed query parameters</td>
</tr>
<tr>
<td><code>headers</code></td>
<td>Map</td>
<td>Request headers</td>
</tr>
<tr>
<td><code>body</code></td>
<td>String</td>
<td>Raw request body</td>
</tr>
<tr>
<td><code>params</code></td>
<td>Map</td>
<td>Route parameters from URL</td>
</tr>
<tr>
<td><code>cookies</code></td>
<td>Map</td>
<td>Parsed cookies</td>
</tr>
<tr>
<td><code>session</code></td>
<td>Session</td>
<td>Session data</td>
</tr>
</table>
<h3>Methods</h3>
<div class="method-signature">
<span class="method-name">header</span>(<span class="param">name</span>) → <span class="type">String|null</span>
</div>
<p>Gets header value (case-insensitive).</p>
<div class="method-signature">
<span class="method-name">json</span><span class="type">Map|List</span>
</div>
<p>Parses body as JSON (cached).</p>
<div class="method-signature">
<span class="method-name">form</span><span class="type">Map</span>
</div>
<p>Parses body as form data (cached).</p>
<h2>Response Class</h2>
<div class="class-header">
<h3>Response</h3>
<p>HTTP response builder</p>
</div>
<h3>Static Factory Methods</h3>
<div class="method-signature">
<span class="method-name">Response.text</span>(<span class="param">content</span>) → <span class="type">Response</span>
</div>
<p>Creates plain text response.</p>
<div class="method-signature">
<span class="method-name">Response.html</span>(<span class="param">content</span>) → <span class="type">Response</span>
</div>
<p>Creates HTML response.</p>
<div class="method-signature">
<span class="method-name">Response.json</span>(<span class="param">data</span>) → <span class="type">Response</span>
</div>
<p>Creates JSON response (automatically stringifies).</p>
<div class="method-signature">
<span class="method-name">Response.redirect</span>(<span class="param">url</span>) → <span class="type">Response</span>
</div>
<div class="method-signature">
<span class="method-name">Response.redirect</span>(<span class="param">url</span>, <span class="param">status</span>) → <span class="type">Response</span>
</div>
<p>Creates redirect response (default 302).</p>
<div class="method-signature">
<span class="method-name">Response.file</span>(<span class="param">path</span>) → <span class="type">Response</span>
</div>
<div class="method-signature">
<span class="method-name">Response.file</span>(<span class="param">path</span>, <span class="param">contentType</span>) → <span class="type">Response</span>
</div>
<p>Serves a file. Auto-detects content type if not specified.</p>
<h3>Instance Methods</h3>
<div class="method-signature">
<span class="method-name">header</span>(<span class="param">name</span>, <span class="param">value</span>) → <span class="type">Response</span>
</div>
<p>Sets a response header.</p>
<div class="method-signature">
<span class="method-name">cookie</span>(<span class="param">name</span>, <span class="param">value</span>) → <span class="type">Response</span>
</div>
<div class="method-signature">
<span class="method-name">cookie</span>(<span class="param">name</span>, <span class="param">value</span>, <span class="param">options</span>) → <span class="type">Response</span>
</div>
<p>Sets a cookie. Options: <code>path</code>, <code>maxAge</code>, <code>httpOnly</code>, <code>secure</code>.</p>
<h3>Properties</h3>
<div class="method-signature">
<span class="method-name">status</span><span class="type">Num</span>
</div>
<p>Gets or sets HTTP status code.</p>
<div class="method-signature">
<span class="method-name">body</span><span class="type">String</span>
</div>
<p>Gets or sets response body.</p>
<h2>View Class</h2>
<div class="class-header">
<h3>View</h3>
<p>Base class for class-based views</p>
</div>
<p>Subclass and override methods for each HTTP method:</p>
<pre><code>class UserView is View {
get(request) {
return Response.json({"users": []})
}
post(request) {
var data = request.json
return Response.json({"created": data})
}
}</code></pre>
<h2>Session Class</h2>
<div class="class-header">
<h3>Session</h3>
<p>Session data storage</p>
</div>
<h3>Properties and Methods</h3>
<div class="method-signature">
<span class="method-name">id</span><span class="type">String</span>
</div>
<p>The session ID.</p>
<div class="method-signature">
<span class="method-name">[key]</span><span class="type">any</span>
</div>
<p>Gets session value.</p>
<div class="method-signature">
<span class="method-name">[key]=</span>(<span class="param">value</span>)
</div>
<p>Sets session value.</p>
<div class="method-signature">
<span class="method-name">remove</span>(<span class="param">key</span>)
</div>
<p>Removes a session key.</p>
<h2>Client Class</h2>
<div class="class-header">
<h3>Client</h3>
<p>HTTP client for making requests</p>
</div>
<h3>Static Methods</h3>
<div class="method-signature">
<span class="method-name">Client.get</span>(<span class="param">url</span>) → <span class="type">Map</span>
</div>
<div class="method-signature">
<span class="method-name">Client.get</span>(<span class="param">url</span>, <span class="param">options</span>) → <span class="type">Map</span>
</div>
<p>Makes GET request.</p>
<div class="method-signature">
<span class="method-name">Client.post</span>(<span class="param">url</span>) → <span class="type">Map</span>
</div>
<div class="method-signature">
<span class="method-name">Client.post</span>(<span class="param">url</span>, <span class="param">options</span>) → <span class="type">Map</span>
</div>
<p>Makes POST request.</p>
<div class="method-signature">
<span class="method-name">Client.put</span>(<span class="param">url</span>, <span class="param">options</span>) → <span class="type">Map</span>
</div>
<p>Makes PUT request.</p>
<div class="method-signature">
<span class="method-name">Client.delete</span>(<span class="param">url</span>, <span class="param">options</span>) → <span class="type">Map</span>
</div>
<p>Makes DELETE request.</p>
<h3>Client Options</h3>
<table>
<tr>
<th>Option</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td><code>headers</code></td>
<td>Map</td>
<td>Request headers</td>
</tr>
<tr>
<td><code>body</code></td>
<td>String</td>
<td>Request body</td>
</tr>
<tr>
<td><code>json</code></td>
<td>Map/List</td>
<td>JSON body (auto-stringifies and sets Content-Type)</td>
</tr>
</table>
<h3>Response Format</h3>
<pre><code>{
"status": 200,
"headers": {"Content-Type": "application/json"},
"body": "{...}"
}</code></pre>
<h2>Examples</h2>
<h3>Basic Server</h3>
<pre><code>import "web" for Application, Response
var app = Application.new()
app.get("/", Fn.new { |req|
return Response.html("&lt;h1&gt;Hello World&lt;/h1&gt;")
})
app.get("/api/users", Fn.new { |req|
return Response.json({"users": ["Alice", "Bob"]})
})
app.run("0.0.0.0", 8080)</code></pre>
<h3>Route Parameters</h3>
<pre><code>import "web" for Application, Response
var app = Application.new()
app.get("/users/:id", Fn.new { |req|
var userId = req.params["id"]
return Response.json({"id": userId})
})
app.get("/files/*", Fn.new { |req|
return Response.text("Wildcard route")
})
app.run("0.0.0.0", 8080)</code></pre>
<h3>Class-Based Views</h3>
<pre><code>import "web" for Application, Response, View
class ArticleView is View {
get(request) {
return Response.json({"articles": []})
}
post(request) {
var data = request.json
return Response.json({"created": data})
}
}
var app = Application.new()
app.addView("/articles", ArticleView)
app.run("0.0.0.0", 8080)</code></pre>
<h3>Sessions</h3>
<pre><code>import "web" for Application, Response
var app = Application.new()
app.get("/login", Fn.new { |req|
req.session["user"] = "Alice"
return Response.text("Logged in")
})
app.get("/profile", Fn.new { |req|
var user = req.session["user"]
if (user == null) {
return Response.redirect("/login")
}
return Response.text("Hello, %(user)")
})
app.run("0.0.0.0", 8080)</code></pre>
<h3>Middleware</h3>
<pre><code>import "web" for Application, Response
var app = Application.new()
app.use(Fn.new { |req|
System.print("%(req.method) %(req.path)")
return null
})
app.use(Fn.new { |req|
if (req.path.startsWith("/admin") &amp;&amp; !req.session["isAdmin"]) {
return Response.redirect("/login")
}
return null
})
app.get("/", Fn.new { |req|
return Response.text("Home")
})
app.run("0.0.0.0", 8080)</code></pre>
<h3>HTTP Client</h3>
<pre><code>import "web" for Client
import "json" for Json
var response = Client.get("https://api.example.com/data")
System.print("Status: %(response["status"])")
System.print("Body: %(response["body"])")
var postResponse = Client.post("https://api.example.com/users", {
"json": {"name": "Alice", "email": "alice@example.com"}
})
var data = Json.parse(postResponse["body"])</code></pre>
<div class="admonition note">
<div class="admonition-title">Note</div>
<p>Sessions are stored in memory by default. Data is lost when the server restarts. For production, consider persisting session data to a database.</p>
</div>
</article>
<footer class="page-footer">
<a href="markdown.html" class="prev">markdown</a>
<a href="index.html" class="next">Overview</a>
</footer>
</main>
</div>
<script src="../js/main.js"></script>
</body>
</html>

View File

@ -146,6 +146,8 @@ OBJECTS += $(OBJDIR)/signal_module.o
OBJECTS += $(OBJDIR)/sqlite3.o
OBJECTS += $(OBJDIR)/sqlite_module.o
OBJECTS += $(OBJDIR)/subprocess.o
OBJECTS += $(OBJDIR)/strutil.o
OBJECTS += $(OBJDIR)/pathlib.o
OBJECTS += $(OBJDIR)/tls.o
OBJECTS += $(OBJDIR)/stream.o
OBJECTS += $(OBJDIR)/strscpy.o
@ -438,6 +440,12 @@ $(OBJDIR)/sqlite3.o: ../../deps/sqlite/sqlite3.c
$(OBJDIR)/sqlite_module.o: ../../src/module/sqlite_module.c
@echo $(notdir $<)
$(SILENT) $(CC) $(ALL_CFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<"
$(OBJDIR)/strutil.o: ../../src/module/strutil.c
@echo $(notdir $<)
$(SILENT) $(CC) $(ALL_CFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<"
$(OBJDIR)/pathlib.o: ../../src/module/pathlib.c
@echo $(notdir $<)
$(SILENT) $(CC) $(ALL_CFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<"
$(OBJDIR)/tls.o: ../../src/module/tls.c
@echo $(notdir $<)
$(SILENT) $(CC) $(ALL_CFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<"

View File

@ -5,6 +5,7 @@
#include "argparse.wren.inc"
#include "base64.wren.inc"
#include "strutil.wren.inc"
#include "bytes.wren.inc"
#include "crypto.wren.inc"
#include "dataset.wren.inc"
@ -20,6 +21,7 @@
#include "math.wren.inc"
#include "net.wren.inc"
#include "os.wren.inc"
#include "pathlib.wren.inc"
#include "regex.wren.inc"
#include "repl.wren.inc"
#include "scheduler.wren.inc"
@ -163,6 +165,29 @@ extern void tlsSocketConnect(WrenVM* vm);
extern void tlsSocketWrite(WrenVM* vm);
extern void tlsSocketRead(WrenVM* vm);
extern void tlsSocketClose(WrenVM* vm);
extern void strutilToLower(WrenVM* vm);
extern void strutilToUpper(WrenVM* vm);
extern void strutilHexEncode(WrenVM* vm);
extern void strutilHexDecode(WrenVM* vm);
extern void strutilRepeat(WrenVM* vm);
extern void strutilPadLeft(WrenVM* vm);
extern void strutilPadRight(WrenVM* vm);
extern void strutilEscapeHtml(WrenVM* vm);
extern void strutilEscapeJson(WrenVM* vm);
extern void strutilUrlEncode(WrenVM* vm);
extern void strutilUrlDecode(WrenVM* vm);
extern void pathlibIsSymlink(WrenVM* vm);
extern void pathlibLstat(WrenVM* vm);
extern void pathlibTouch(WrenVM* vm);
extern void pathlibRename(WrenVM* vm);
extern void pathlibChmod(WrenVM* vm);
extern void pathlibSymlinkTo(WrenVM* vm);
extern void pathlibHardlinkTo(WrenVM* vm);
extern void pathlibReadlink(WrenVM* vm);
extern void pathlibSamefile(WrenVM* vm);
extern void pathlibGlob(WrenVM* vm);
extern void pathlibOwner(WrenVM* vm);
extern void pathlibGroup(WrenVM* vm);
// The maximum number of foreign methods a single class defines. Ideally, we
// would use variable-length arrays for each class in the table below, but
@ -402,6 +427,22 @@ static ModuleRegistry modules[] =
STATIC_METHOD("version", processVersion)
END_CLASS
END_MODULE
MODULE(pathlib)
CLASS(Path)
STATIC_METHOD("isSymlink_(_,_)", pathlibIsSymlink)
STATIC_METHOD("lstat_(_,_)", pathlibLstat)
STATIC_METHOD("touch_(_,_)", pathlibTouch)
STATIC_METHOD("rename_(_,_,_)", pathlibRename)
STATIC_METHOD("chmod_(_,_,_)", pathlibChmod)
STATIC_METHOD("symlinkTo_(_,_,_)", pathlibSymlinkTo)
STATIC_METHOD("hardlinkTo_(_,_,_)", pathlibHardlinkTo)
STATIC_METHOD("readlink_(_,_)", pathlibReadlink)
STATIC_METHOD("samefile_(_,_,_)", pathlibSamefile)
STATIC_METHOD("glob_(_,_,_,_)", pathlibGlob)
STATIC_METHOD("owner_(_,_)", pathlibOwner)
STATIC_METHOD("group_(_,_)", pathlibGroup)
END_CLASS
END_MODULE
MODULE(regex)
CLASS(Regex)
ALLOCATE(regexAllocate)
@ -430,6 +471,21 @@ static ModuleRegistry modules[] =
STATIC_METHOD("reset_(_)", signalReset)
END_CLASS
END_MODULE
MODULE(strutil)
CLASS(Str)
STATIC_METHOD("toLower(_)", strutilToLower)
STATIC_METHOD("toUpper(_)", strutilToUpper)
STATIC_METHOD("hexEncode(_)", strutilHexEncode)
STATIC_METHOD("hexDecode(_)", strutilHexDecode)
STATIC_METHOD("repeat(_,_)", strutilRepeat)
STATIC_METHOD("padLeft(_,_,_)", strutilPadLeft)
STATIC_METHOD("padRight(_,_,_)", strutilPadRight)
STATIC_METHOD("escapeHtml(_)", strutilEscapeHtml)
STATIC_METHOD("escapeJson(_)", strutilEscapeJson)
STATIC_METHOD("urlEncode(_)", strutilUrlEncode)
STATIC_METHOD("urlDecode(_)", strutilUrlDecode)
END_CLASS
END_MODULE
MODULE(sqlite)
CLASS(Database)
ALLOCATE(sqliteAllocate)

View File

@ -1,6 +1,7 @@
// retoor <retoor@molodetz.nl>
import "os" for Process
import "strutil" for Str
class ArgumentParser {
construct new() {
@ -269,9 +270,5 @@ class ArgumentParser {
return " " + parts.join(" ")
}
pad_(str, width) {
var result = str
while (result.count < width) result = result + " "
return result
}
pad_(str, width) { Str.padRight(str, width, " ") }
}

View File

@ -5,6 +5,7 @@ static const char* argparseModuleSource =
"// retoor <retoor@molodetz.nl>\n"
"\n"
"import \"os\" for Process\n"
"import \"strutil\" for Str\n"
"\n"
"class ArgumentParser {\n"
" construct new() {\n"
@ -273,9 +274,5 @@ static const char* argparseModuleSource =
" return \" \" + parts.join(\" \")\n"
" }\n"
"\n"
" pad_(str, width) {\n"
" var result = str\n"
" while (result.count < width) result = result + \" \"\n"
" return result\n"
" }\n"
" pad_(str, width) { Str.padRight(str, width, \" \") }\n"
"}\n";

View File

@ -0,0 +1,390 @@
// Please do not edit this file. It has been generated automatically
// from `create_training_data.wren` using `util/wren_to_c_string.py`
static const char* create_training_dataModuleSource =
"// retoor <retoor@molodetz.nl>\n"
"\n"
"import \"argparse\" for ArgumentParser\n"
"import \"crypto\" for Hash\n"
"import \"json\" for Json\n"
"import \"regex\" for Regex\n"
"import \"io\" for File, Directory\n"
"import \"scheduler\" for Scheduler\n"
"\n"
"var SYSTEM_PROMPT = \"You are an expert assistant for the Wren-CLI language and its extended modules. You answer with correct Wren syntax and semantics, and you understand the Wren-CLI standard library plus the custom modules documented in the provided manual/tests. When asked for expected output, provide exactly the printed output lines in order. When asked about APIs, provide concise explanations and minimal working examples.\"\n"
"\n"
"class Sample {\n"
" construct new(user, assistant) {\n"
" _user = user\n"
" _assistant = assistant\n"
" }\n"
"\n"
" user { _user }\n"
" assistant { _assistant }\n"
"}\n"
"\n"
"class PseudoRandom {\n"
" construct new(seed) {\n"
" _state = seed\n"
" }\n"
"\n"
" next() {\n"
" _state = (_state * 1103515245 + 12345) & 0x7fffffff\n"
" return _state\n"
" }\n"
"\n"
" shuffle(list) {\n"
" if (list.count < 2) return\n"
" var i = list.count - 1\n"
" while (i > 0) {\n"
" var j = next() % (i + 1)\n"
" var temp = list[i]\n"
" list[i] = list[j]\n"
" list[j] = temp\n"
" i = i - 1\n"
" }\n"
" }\n"
"}\n"
"\n"
"class TrainingDataGenerator {\n"
" static sha1Text(s) {\n"
" return Hash.toHex(Hash.sha1(s))\n"
" }\n"
"\n"
" static normalizeNewlines(s) {\n"
" var result = s.replace(\"\\r\\n\", \"\\n\")\n"
" result = result.replace(\"\\r\", \"\\n\")\n"
" return result\n"
" }\n"
"\n"
" static safeTrim(s, maxChars) {\n"
" if (maxChars <= 0) return s\n"
" if (s.count <= maxChars) return s\n"
" return s[0...(maxChars - 1)] + \"\"\n"
" }\n"
"\n"
" static writeJsonl(samples, outPath, systemPrompt) {\n"
" var path = outPath is String ? outPath : outPath.toString\n"
" var dirPath = path[0...(path.lastIndexOf(\"/\") + 1)]\n"
" if (dirPath.count > 0 && !Directory.exists(dirPath)) {\n"
" Directory.create(dirPath)\n"
" }\n"
" File.create(path) {|f|\n"
" for (smp in samples) {\n"
" var obj = {\n"
" \"messages\": [\n"
" {\"role\": \"system\", \"content\": systemPrompt},\n"
" {\"role\": \"user\", \"content\": smp.user},\n"
" {\"role\": \"assistant\", \"content\": smp.assistant}\n"
" ]\n"
" }\n"
" f.writeBytes(Json.stringify(obj) + \"\\n\", 0)\n"
" }\n"
" }\n"
" }\n"
"\n"
" static validateJsonl(path) {\n"
" var pathStr = path is String ? path : path.toString\n"
" var content = File.read(pathStr)\n"
" var lines = content.split(\"\\n\")\n"
" for (i in 0...lines.count) {\n"
" var line = lines[i].trim()\n"
" if (line.isEmpty) {\n"
" Fiber.abort(\"%(pathStr):%(i + 1): empty line\")\n"
" }\n"
" var obj = null\n"
" Fiber.new {\n"
" obj = Json.parse(line)\n"
" }.try()\n"
" if (obj == null) {\n"
" Fiber.abort(\"%(pathStr):%(i + 1): invalid JSON\")\n"
" }\n"
"\n"
" if (!obj.containsKey(\"messages\") || !(obj[\"messages\"] is List)) {\n"
" Fiber.abort(\"%(pathStr):%(i + 1): missing/invalid 'messages' array\")\n"
" }\n"
"\n"
" var messages = obj[\"messages\"]\n"
" for (j in 0...messages.count) {\n"
" var msg = messages[j]\n"
" if (!(msg is Map)) {\n"
" Fiber.abort(\"%(pathStr):%(i + 1): messages[%(j)] not an object\")\n"
" }\n"
" var role = msg[\"role\"]\n"
" if (role != \"system\" && role != \"user\" && role != \"assistant\") {\n"
" Fiber.abort(\"%(pathStr):%(i + 1): messages[%(j)].role invalid\")\n"
" }\n"
" if (!(msg[\"content\"] is String)) {\n"
" Fiber.abort(\"%(pathStr):%(i + 1): messages[%(j)].content must be a string\")\n"
" }\n"
" }\n"
" }\n"
" }\n"
"\n"
" static splitTestBlocks(lines) {\n"
" var blocks = []\n"
" var cur = []\n"
" var expectRe = Regex.new(\"\\\\bexpect\\\\b\\\\s*(.*)\\\\s*$\")\n"
" var headerNoiseRe = Regex.new(\"^\\\\s*(retoor\\\\s+retoormolodetz\\\\.nl|TITLE\\\\s+Subtitle\\\\.\\\\.\\\\.)\\\\s*$\")\n"
"\n"
" var flush = Fn.new {\n"
" if (!cur.isEmpty) {\n"
" var hasExpect = false\n"
" for (ln in cur) {\n"
" if (ln.contains(\"expect\")) {\n"
" hasExpect = true\n"
" break\n"
" }\n"
" }\n"
" if (hasExpect) blocks.add(cur)\n"
" }\n"
" cur = []\n"
" }\n"
"\n"
" for (raw in lines) {\n"
" var ln = raw.replace(\"\\n\", \"\").replace(\"\\r\", \"\")\n"
"\n"
" var m = headerNoiseRe.match(ln.trim())\n"
" if (m != null) {\n"
" flush.call()\n"
" continue\n"
" }\n"
"\n"
" if (ln.trim().isEmpty) {\n"
" flush.call()\n"
" continue\n"
" }\n"
"\n"
" var hasExpect = false\n"
" for (x in cur) {\n"
" if (x.contains(\"expect\")) {\n"
" hasExpect = true\n"
" break\n"
" }\n"
" }\n"
" if (hasExpect && ln.lstrip().startsWith(\"import \")) {\n"
" flush.call()\n"
" }\n"
"\n"
" cur.add(ln)\n"
" }\n"
"\n"
" flush.call()\n"
" return blocks\n"
" }\n"
"\n"
" static parseWrenTestsMarkdown(path, maxCodeChars, maxAnswerChars) {\n"
" var content = File.read(path)\n"
" var text = normalizeNewlines(content)\n"
" var lines = text.split(\"\\n\")\n"
" var blocks = splitTestBlocks(lines)\n"
" var expectRe = Regex.new(\"\\\\bexpect\\\\b\\\\s*(.*)\\\\s*$\")\n"
"\n"
" var samples = []\n"
" for (block in blocks) {\n"
" var expected = []\n"
" for (ln in block) {\n"
" var m = expectRe.match(ln)\n"
" if (m != null) {\n"
" expected.add(m.group(1).trim())\n"
" }\n"
" }\n"
"\n"
" if (expected.isEmpty) continue\n"
"\n"
" var code = block.join(\"\\n\").trim()\n"
" code = safeTrim(code, maxCodeChars)\n"
"\n"
" var assistantParts = []\n"
" for (exp in expected) {\n"
" assistantParts.add(safeTrim(exp, maxAnswerChars))\n"
" }\n"
" var assistant = assistantParts.join(\"\\n\").trim()\n"
"\n"
" var user = \"Given this Wren-CLI program, what output should it produce?\\nReturn one output line per expectation, in order.\\n\\n```wren\\n%(code)\\n```\"\n"
"\n"
" samples.add(Sample.new(user, assistant))\n"
" }\n"
"\n"
" return samples\n"
" }\n"
"\n"
" static parseManualHtml(path, maxExampleChars, maxAnswerChars) {\n"
" var content = File.read(path)\n"
" var samples = []\n"
"\n"
" var articleRe = Regex.new(\"<article>([\\\\s\\\\S]*?)</article>\", \"s\")\n"
" var articles = articleRe.matchAll(content)\n"
"\n"
" for (articleMatch in articles) {\n"
" var articleContent = articleMatch.group(1)\n"
"\n"
" var h1Re = Regex.new(\"<h1[^>]*>([\\\\s\\\\S]*?)</h1>\", \"s\")\n"
" var h1Match = h1Re.match(articleContent)\n"
" if (h1Match == null) continue\n"
"\n"
" var h1Content = h1Match.group(1)\n"
" var moduleName = stripHtmlTags(h1Content)\n"
" if (moduleName.isEmpty) continue\n"
"\n"
" var pRe = Regex.new(\"<p[^>]*>([\\\\s\\\\S]*?)</p>\", \"s\")\n"
" var firstPMatch = pRe.match(articleContent)\n"
" var moduleDesc = \"\"\n"
" if (firstPMatch != null) {\n"
" moduleDesc = stripHtmlTags(firstPMatch.group(1))\n"
" }\n"
"\n"
" var sigRe = Regex.new('<div class=\"method-signature\"[^>]*>([\\\\s\\\\S]*?)</div>', \"s\")\n"
" var sigs = sigRe.matchAll(articleContent)\n"
"\n"
" for (sigMatch in sigs) {\n"
" var sigContent = sigMatch.group(1)\n"
"\n"
" var nameRe = Regex.new('<span class=\"method-name\"[^>]*>([\\\\s\\\\S]*?)</span>', \"s\")\n"
" var nameMatch = nameRe.match(sigContent)\n"
" if (nameMatch == null) continue\n"
"\n"
" var methodName = stripHtmlTags(nameMatch.group(1))\n"
" if (methodName.isEmpty) continue\n"
"\n"
" var methodDesc = \"\"\n"
" var descPRe = Regex.new('<p[^>]*>([\\\\s\\\\S]*?)</p>', \"s\")\n"
" var afterSig = articleContent[sigMatch.end..-1]\n"
" var descMatch = descPRe.match(afterSig)\n"
" if (descMatch != null) {\n"
" methodDesc = stripHtmlTags(descMatch.group(1))\n"
" }\n"
"\n"
" var preRe = Regex.new('<pre[^>]*><code[^>]*>([\\\\s\\\\S]*?)</code></pre>', \"s\")\n"
" var preMatch = preRe.match(afterSig)\n"
" var example = \"\"\n"
" if (preMatch != null) {\n"
" example = stripHtmlEntities(preMatch.group(1))\n"
" example = safeTrim(example, maxExampleChars)\n"
" }\n"
"\n"
" var assistantParts = []\n"
" if (!moduleDesc.isEmpty) assistantParts.add(moduleDesc)\n"
" if (!methodDesc.isEmpty) assistantParts.add(methodDesc)\n"
" if (!example.isEmpty) assistantParts.add(\"Example:\\n```wren\\n%(example)\\n```\")\n"
"\n"
" var assistant = assistantParts.join(\"\\n\\n\").trim()\n"
" assistant = safeTrim(assistant, maxAnswerChars)\n"
"\n"
" var user = \"In the Wren-CLI `%(moduleName)` module, how do I use `%(methodName)`?\\nGive a short explanation and a minimal example.\"\n"
"\n"
" if (assistant.count < 20) continue\n"
"\n"
" samples.add(Sample.new(user, assistant))\n"
" }\n"
" }\n"
"\n"
" return samples\n"
" }\n"
"\n"
" static stripHtmlTags(html) {\n"
" var result = html\n"
" var tagRe = Regex.new(\"<[^>]+>\")\n"
" while (true) {\n"
" var before = result\n"
" result = tagRe.replaceAll(result, \"\")\n"
" if (result == before) break\n"
" }\n"
" result = result.replace(\"&nbsp;\", \" \")\n"
" result = result.replace(\"&lt;\", \"<\")\n"
" result = result.replace(\"&gt;\", \">\")\n"
" result = result.replace(\"&amp;\", \"&\")\n"
" result = result.replace(\"&quot;\", \"\\\"\")\n"
" result = result.replace(\"&#39;\", \"'\")\n"
" return result.trim()\n"
" }\n"
"\n"
" static stripHtmlEntities(html) {\n"
" var result = html\n"
" result = result.replace(\"&lt;\", \"<\")\n"
" result = result.replace(\"&gt;\", \">\")\n"
" result = result.replace(\"&amp;\", \"&\")\n"
" result = result.replace(\"&quot;\", \"\\\"\")\n"
" result = result.replace(\"&#39;\", \"'\")\n"
" return result\n"
" }\n"
"\n"
" static dedupe(samples) {\n"
" var seen = {}\n"
" var out = []\n"
" for (s in samples) {\n"
" var key = sha1Text(s.user + \"\\n---\\n\" + s.assistant)\n"
" if (!seen.containsKey(key)) {\n"
" seen[key] = true\n"
" out.add(s)\n"
" }\n"
" }\n"
" return out\n"
" }\n"
"\n"
" static main(args) {\n"
" var parser = ArgumentParser.new(\"Create training data from Wren documentation and tests\")\n"
" parser.addArgument(\"--tests-md\", {\"action\": \"append\", \"type\": \"string\", \"help\": \"Path(s) to wren_tests.md-like files\"})\n"
" parser.addArgument(\"--manual-html\", {\"action\": \"append\", \"type\": \"string\", \"help\": \"Path(s) to manual.html-like files\"})\n"
" parser.addArgument(\"--out-train\", {\"type\": \"string\", \"default\": \"train.jsonl\", \"help\": \"Output training file\"})\n"
" parser.addArgument(\"--out-val\", {\"type\": \"string\", \"help\": \"Output validation file\"})\n"
" parser.addArgument(\"--val-ratio\", {\"type\": \"float\", \"default\": 0.02, \"help\": \"Validation ratio\"})\n"
" parser.addArgument(\"--seed\", {\"type\": \"int\", \"default\": 42, \"help\": \"Random seed\"})\n"
" parser.addArgument(\"--max-code-chars\", {\"type\": \"int\", \"default\": 12000, \"help\": \"Max code characters\"})\n"
" parser.addArgument(\"--max-example-chars\", {\"type\": \"int\", \"default\": 2000, \"help\": \"Max example characters\"})\n"
" parser.addArgument(\"--max-answer-chars\", {\"type\": \"int\", \"default\": 6000, \"help\": \"Max answer characters\"})\n"
" parser.addArgument(\"--system-prompt\", {\"type\": \"string\", \"default\": SYSTEM_PROMPT, \"help\": \"System prompt\"})\n"
" parser.addArgument(\"--validate\", {\"action\": \"storeTrue\", \"help\": \"Validate output JSONL\"})\n"
"\n"
" var parsed = parser.parseArgs(args)\n"
"\n"
" var allSamples = []\n"
"\n"
" if (parsed.containsKey(\"tests_md\") && parsed[\"tests_md\"] != null) {\n"
" var testsMdPaths = parsed[\"tests_md\"]\n"
" for (p in testsMdPaths) {\n"
" allSamples.addAll(parseWrenTestsMarkdown(p, parsed[\"max_code_chars\"], parsed[\"max_answer_chars\"]))\n"
" }\n"
" }\n"
"\n"
" if (parsed.containsKey(\"manual_html\") && parsed[\"manual_html\"] != null) {\n"
" var manualHtmlPaths = parsed[\"manual_html\"]\n"
" for (p in manualHtmlPaths) {\n"
" allSamples.addAll(parseManualHtml(p, parsed[\"max_example_chars\"], parsed[\"max_answer_chars\"]))\n"
" }\n"
" }\n"
"\n"
" allSamples = dedupe(allSamples)\n"
"\n"
" var rng = PseudoRandom.new(parsed[\"seed\"])\n"
" rng.shuffle(allSamples)\n"
"\n"
" if (parsed.containsKey(\"out_val\") && parsed[\"out_val\"] != null && parsed[\"val_ratio\"] > 0 && parsed[\"val_ratio\"] < 1) {\n"
" var nVal = (allSamples.count * parsed[\"val_ratio\"]).floor.max(1)\n"
" var val = allSamples[0...nVal]\n"
" var train = allSamples[nVal..-1]\n"
"\n"
" writeJsonl(train, parsed[\"out_train\"], parsed[\"system_prompt\"])\n"
" writeJsonl(val, parsed[\"out_val\"], parsed[\"system_prompt\"])\n"
"\n"
" if (parsed[\"validate\"]) {\n"
" validateJsonl(parsed[\"out_train\"])\n"
" validateJsonl(parsed[\"out_val\"])\n"
" }\n"
"\n"
" System.print(\"Wrote train=%(train.count) to %(parsed[\"out_train\"])\")\n"
" System.print(\"Wrote val=%(val.count) to %(parsed[\"out_val\"])\")\n"
" } else {\n"
" writeJsonl(allSamples, parsed[\"out_train\"], parsed[\"system_prompt\"])\n"
"\n"
" if (parsed[\"validate\"]) {\n"
" validateJsonl(parsed[\"out_train\"])\n"
" }\n"
"\n"
" System.print(\"Wrote train=%(allSamples.count) to %(parsed[\"out_train\"])\")\n"
" }\n"
" }\n"
"}\n"
"\n"
"import \"os\" for Process\n"
"TrainingDataGenerator.main(Process.arguments)\n";

View File

@ -1,6 +1,8 @@
// retoor <retoor@molodetz.nl>
import "scheduler" for Scheduler
import "strutil" for Str
import "bytes" for Bytes
class Crypto {
foreign static randomBytes_(length, fiber)
@ -46,18 +48,5 @@ class Hash {
return sha256_(data)
}
static toHex(bytes) {
var hex = ""
for (b in bytes) {
var hi = (b >> 4) & 0x0F
var lo = b & 0x0F
hex = hex + hexDigit_(hi) + hexDigit_(lo)
}
return hex
}
static hexDigit_(n) {
if (n < 10) return String.fromCodePoint(48 + n)
return String.fromCodePoint(97 + n - 10)
}
static toHex(bytes) { Str.hexEncode(Bytes.fromList(bytes)) }
}

View File

@ -5,6 +5,8 @@ static const char* cryptoModuleSource =
"// retoor <retoor@molodetz.nl>\n"
"\n"
"import \"scheduler\" for Scheduler\n"
"import \"strutil\" for Str\n"
"import \"bytes\" for Bytes\n"
"\n"
"class Crypto {\n"
" foreign static randomBytes_(length, fiber)\n"
@ -50,18 +52,5 @@ static const char* cryptoModuleSource =
" return sha256_(data)\n"
" }\n"
"\n"
" static toHex(bytes) {\n"
" var hex = \"\"\n"
" for (b in bytes) {\n"
" var hi = (b >> 4) & 0x0F\n"
" var lo = b & 0x0F\n"
" hex = hex + hexDigit_(hi) + hexDigit_(lo)\n"
" }\n"
" return hex\n"
" }\n"
"\n"
" static hexDigit_(n) {\n"
" if (n < 10) return String.fromCodePoint(48 + n)\n"
" return String.fromCodePoint(97 + n - 10)\n"
" }\n"
" static toHex(bytes) { Str.hexEncode(Bytes.fromList(bytes)) }\n"
"}\n";

58
src/module/html.wren vendored
View File

@ -1,5 +1,7 @@
// retoor <retoor@molodetz.nl>
import "strutil" for Str
class Html {
static isUnreserved_(c) {
var code = c.codePoints.toList[0]
@ -25,19 +27,7 @@ class Html {
static urlencode(string) {
if (!(string is String)) Fiber.abort("Argument must be a string.")
var result = ""
for (c in string) {
if (isUnreserved_(c)) {
result = result + c
} else if (c == " ") {
result = result + "+"
} else {
for (b in c.bytes) {
result = result + "\%" + hexDigit_((b >> 4) & 0x0F) + hexDigit_(b & 0x0F)
}
}
}
return result
return Str.urlEncode(string)
}
static decodeUtf8_(bytes) {
@ -70,29 +60,7 @@ class Html {
static urldecode(string) {
if (!(string is String)) Fiber.abort("Argument must be a string.")
var bytes = []
var i = 0
var chars = string.toList
while (i < chars.count) {
if (chars[i] == "+") {
bytes.add(32)
i = i + 1
} else if (chars[i] == "\%" && i + 2 < chars.count) {
var hi = hexValue_(chars[i + 1])
var lo = hexValue_(chars[i + 2])
if (hi >= 0 && lo >= 0) {
bytes.add((hi << 4) | lo)
i = i + 3
} else {
for (b in chars[i].bytes) bytes.add(b)
i = i + 1
}
} else {
for (b in chars[i].bytes) bytes.add(b)
i = i + 1
}
}
return decodeUtf8_(bytes)
return Str.urlDecode(string)
}
static slugify(string) {
@ -119,23 +87,7 @@ class Html {
static quote(string) {
if (!(string is String)) Fiber.abort("Argument must be a string.")
var result = ""
for (c in string) {
if (c == "&") {
result = result + "&amp;"
} else if (c == "<") {
result = result + "&lt;"
} else if (c == ">") {
result = result + "&gt;"
} else if (c == "\"") {
result = result + "&quot;"
} else if (c == "'") {
result = result + "&#39;"
} else {
result = result + c
}
}
return result
return Str.escapeHtml(string)
}
static unquote(string) {

View File

@ -4,6 +4,8 @@
static const char* htmlModuleSource =
"// retoor <retoor@molodetz.nl>\n"
"\n"
"import \"strutil\" for Str\n"
"\n"
"class Html {\n"
" static isUnreserved_(c) {\n"
" var code = c.codePoints.toList[0]\n"
@ -29,19 +31,7 @@ static const char* htmlModuleSource =
"\n"
" static urlencode(string) {\n"
" if (!(string is String)) Fiber.abort(\"Argument must be a string.\")\n"
" var result = \"\"\n"
" for (c in string) {\n"
" if (isUnreserved_(c)) {\n"
" result = result + c\n"
" } else if (c == \" \") {\n"
" result = result + \"+\"\n"
" } else {\n"
" for (b in c.bytes) {\n"
" result = result + \"\\%\" + hexDigit_((b >> 4) & 0x0F) + hexDigit_(b & 0x0F)\n"
" }\n"
" }\n"
" }\n"
" return result\n"
" return Str.urlEncode(string)\n"
" }\n"
"\n"
" static decodeUtf8_(bytes) {\n"
@ -74,29 +64,7 @@ static const char* htmlModuleSource =
"\n"
" static urldecode(string) {\n"
" if (!(string is String)) Fiber.abort(\"Argument must be a string.\")\n"
" var bytes = []\n"
" var i = 0\n"
" var chars = string.toList\n"
" while (i < chars.count) {\n"
" if (chars[i] == \"+\") {\n"
" bytes.add(32)\n"
" i = i + 1\n"
" } else if (chars[i] == \"\\%\" && i + 2 < chars.count) {\n"
" var hi = hexValue_(chars[i + 1])\n"
" var lo = hexValue_(chars[i + 2])\n"
" if (hi >= 0 && lo >= 0) {\n"
" bytes.add((hi << 4) | lo)\n"
" i = i + 3\n"
" } else {\n"
" for (b in chars[i].bytes) bytes.add(b)\n"
" i = i + 1\n"
" }\n"
" } else {\n"
" for (b in chars[i].bytes) bytes.add(b)\n"
" i = i + 1\n"
" }\n"
" }\n"
" return decodeUtf8_(bytes)\n"
" return Str.urlDecode(string)\n"
" }\n"
"\n"
" static slugify(string) {\n"
@ -123,23 +91,7 @@ static const char* htmlModuleSource =
"\n"
" static quote(string) {\n"
" if (!(string is String)) Fiber.abort(\"Argument must be a string.\")\n"
" var result = \"\"\n"
" for (c in string) {\n"
" if (c == \"&\") {\n"
" result = result + \"&amp;\"\n"
" } else if (c == \"<\") {\n"
" result = result + \"&lt;\"\n"
" } else if (c == \">\") {\n"
" result = result + \"&gt;\"\n"
" } else if (c == \"\\\"\") {\n"
" result = result + \"&quot;\"\n"
" } else if (c == \"'\") {\n"
" result = result + \"&#39;\"\n"
" } else {\n"
" result = result + c\n"
" }\n"
" }\n"
" return result\n"
" return Str.escapeHtml(string)\n"
" }\n"
"\n"
" static unquote(string) {\n"

14
src/module/http.wren vendored
View File

@ -5,6 +5,7 @@ import "tls" for TlsSocket
import "dns" for Dns
import "json" for Json
import "base64" for Base64
import "strutil" for Str
class Url {
construct parse(url) {
@ -44,18 +45,7 @@ class Url {
}
}
static toLower_(str) {
var result = ""
for (c in str) {
var cp = c.codePoints[0]
if (cp >= 65 && cp <= 90) {
result = result + String.fromCodePoint(cp + 32)
} else {
result = result + c
}
}
return result
}
static toLower_(str) { Str.toLower(str) }
scheme { _scheme }
host { _host }

View File

@ -1,5 +1,5 @@
// Please do not edit this file. It has been generated automatically
// from `/home/retoor/projects/wren-cli/src/module/http.wren` using `util/wren_to_c_string.py`
// from `src/module/http.wren` using `util/wren_to_c_string.py`
static const char* httpModuleSource =
"// retoor <retoor@molodetz.nl>\n"
@ -9,6 +9,7 @@ static const char* httpModuleSource =
"import \"dns\" for Dns\n"
"import \"json\" for Json\n"
"import \"base64\" for Base64\n"
"import \"strutil\" for Str\n"
"\n"
"class Url {\n"
" construct parse(url) {\n"
@ -48,18 +49,7 @@ static const char* httpModuleSource =
" }\n"
" }\n"
"\n"
" static toLower_(str) {\n"
" var result = \"\"\n"
" for (c in str) {\n"
" var cp = c.codePoints[0]\n"
" if (cp >= 65 && cp <= 90) {\n"
" result = result + String.fromCodePoint(cp + 32)\n"
" } else {\n"
" result = result + c\n"
" }\n"
" }\n"
" return result\n"
" }\n"
" static toLower_(str) { Str.toLower(str) }\n"
"\n"
" scheme { _scheme }\n"
" host { _host }\n"

26
src/module/json.wren vendored
View File

@ -1,5 +1,7 @@
// retoor <retoor@molodetz.nl>
import "strutil" for Str
class Json {
foreign static parse(string)
@ -35,29 +37,7 @@ class Json {
return "null"
}
static escapeString_(s) {
var result = "\""
for (c in s) {
if (c == "\"") {
result = result + "\\\""
} else if (c == "\\") {
result = result + "\\\\"
} else if (c == "\b") {
result = result + "\\b"
} else if (c == "\f") {
result = result + "\\f"
} else if (c == "\n") {
result = result + "\\n"
} else if (c == "\r") {
result = result + "\\r"
} else if (c == "\t") {
result = result + "\\t"
} else {
result = result + c
}
}
return result + "\""
}
static escapeString_(s) { Str.escapeJson(s) }
static stringifyList_(list, indent, currentIndent) {
if (list.count == 0) return "[]"

View File

@ -4,6 +4,8 @@
static const char* jsonModuleSource =
"// retoor <retoor@molodetz.nl>\n"
"\n"
"import \"strutil\" for Str\n"
"\n"
"class Json {\n"
" foreign static parse(string)\n"
"\n"
@ -39,29 +41,7 @@ static const char* jsonModuleSource =
" return \"null\"\n"
" }\n"
"\n"
" static escapeString_(s) {\n"
" var result = \"\\\"\"\n"
" for (c in s) {\n"
" if (c == \"\\\"\") {\n"
" result = result + \"\\\\\\\"\"\n"
" } else if (c == \"\\\\\") {\n"
" result = result + \"\\\\\\\\\"\n"
" } else if (c == \"\\b\") {\n"
" result = result + \"\\\\b\"\n"
" } else if (c == \"\\f\") {\n"
" result = result + \"\\\\f\"\n"
" } else if (c == \"\\n\") {\n"
" result = result + \"\\\\n\"\n"
" } else if (c == \"\\r\") {\n"
" result = result + \"\\\\r\"\n"
" } else if (c == \"\\t\") {\n"
" result = result + \"\\\\t\"\n"
" } else {\n"
" result = result + c\n"
" }\n"
" }\n"
" return result + \"\\\"\"\n"
" }\n"
" static escapeString_(s) { Str.escapeJson(s) }\n"
"\n"
" static stringifyList_(list, indent, currentIndent) {\n"
" if (list.count == 0) return \"[]\"\n"

View File

@ -325,4 +325,563 @@ class Markdown {
}
return result
}
static fromHtml(html) { fromHtml(html, {}) }
static fromHtml(html, options) {
var stripUnknown = options.containsKey("stripUnknown") ? options["stripUnknown"] : true
var text = html
text = removeHiddenTags_(text)
text = processBlockElements_(text)
text = processInlineFromHtml_(text)
text = Html.unquote(text)
text = cleanupWhitespace_(text)
return text.trim()
}
static removeHiddenTags_(html) {
var result = html
var tags = ["script", "style", "head", "meta", "link", "noscript"]
for (tag in tags) {
result = removeTagWithContent_(result, tag)
}
return result
}
static removeTagWithContent_(html, tag) {
var result = html
var openTag = "<" + tag
var closeTag = "</" + tag + ">"
while (true) {
var startLower = result.indexOf(openTag)
var startUpper = result.indexOf("<" + tag[0].codePoints.toList[0].toString)
var start = -1
var i = 0
while (i < result.count - openTag.count) {
if (matchTagName_(result, i, tag)) {
start = i
break
}
i = i + 1
}
if (start < 0) break
var tagEnd = start
while (tagEnd < result.count && result[tagEnd] != ">") {
tagEnd = tagEnd + 1
}
if (tagEnd >= result.count) break
if (result[tagEnd - 1] == "/") {
result = result[0...start] + result[tagEnd + 1..-1]
continue
}
var closeStart = findCloseTag_(result, tagEnd + 1, tag)
if (closeStart < 0) {
result = result[0...start] + result[tagEnd + 1..-1]
} else {
var closeEnd = closeStart
while (closeEnd < result.count && result[closeEnd] != ">") {
closeEnd = closeEnd + 1
}
result = result[0...start] + result[closeEnd + 1..-1]
}
}
return result
}
static matchTagName_(html, pos, tag) {
if (pos >= html.count || html[pos] != "<") return false
var rest = html[pos + 1..-1].trim()
var tagLower = tag
for (i in 0...tag.count) {
if (i >= rest.count) return false
var c1 = rest[i].codePoints.toList[0]
var c2 = tag[i].codePoints.toList[0]
if (c1 >= 65 && c1 <= 90) c1 = c1 + 32
if (c2 >= 65 && c2 <= 90) c2 = c2 + 32
if (c1 != c2) return false
}
if (tag.count < rest.count) {
var next = rest[tag.count]
if (next != " " && next != ">" && next != "/" && next != "\t" && next != "\n") return false
}
return true
}
static findCloseTag_(html, start, tag) {
var i = start
while (i < html.count - tag.count - 2) {
if (html[i] == "<" && html[i + 1] == "/") {
var match = true
for (j in 0...tag.count) {
var c1 = html[i + 2 + j].codePoints.toList[0]
var c2 = tag[j].codePoints.toList[0]
if (c1 >= 65 && c1 <= 90) c1 = c1 + 32
if (c2 >= 65 && c2 <= 90) c2 = c2 + 32
if (c1 != c2) {
match = false
break
}
}
if (match) return i
}
i = i + 1
}
return -1
}
static processBlockElements_(html) {
var result = html
result = processHeadingsFromHtml_(result)
result = processCodeBlocksFromHtml_(result)
result = processBlockquotesFromHtml_(result)
result = processListsFromHtml_(result)
result = processHrFromHtml_(result)
result = processParagraphsFromHtml_(result)
result = processBrFromHtml_(result)
result = stripContainerTags_(result)
return result
}
static processHeadingsFromHtml_(html) {
var result = html
for (level in 1..6) {
var prefix = ""
for (i in 0...level) prefix = prefix + "#"
result = replaceTag_(result, "h" + level.toString, prefix + " ", "\n\n")
}
return result
}
static processCodeBlocksFromHtml_(html) {
var result = html
while (true) {
var preStart = findTagStart_(result, "pre")
if (preStart < 0) break
var preOpenEnd = preStart
while (preOpenEnd < result.count && result[preOpenEnd] != ">") {
preOpenEnd = preOpenEnd + 1
}
if (preOpenEnd >= result.count) break
var preCloseStart = findCloseTag_(result, preOpenEnd + 1, "pre")
if (preCloseStart < 0) break
var content = result[preOpenEnd + 1...preCloseStart]
var codeStart = findTagStart_(content, "code")
if (codeStart >= 0) {
var codeOpenEnd = codeStart
while (codeOpenEnd < content.count && content[codeOpenEnd] != ">") {
codeOpenEnd = codeOpenEnd + 1
}
var codeCloseStart = findCloseTag_(content, codeOpenEnd + 1, "code")
if (codeCloseStart >= 0) {
content = content[codeOpenEnd + 1...codeCloseStart]
}
}
var preCloseEnd = preCloseStart
while (preCloseEnd < result.count && result[preCloseEnd] != ">") {
preCloseEnd = preCloseEnd + 1
}
var before = result[0...preStart]
var after = result[preCloseEnd + 1..-1]
result = before + "\n```\n" + content.trim() + "\n```\n" + after
}
return result
}
static processBlockquotesFromHtml_(html) {
var result = html
while (true) {
var start = findTagStart_(result, "blockquote")
if (start < 0) break
var openEnd = start
while (openEnd < result.count && result[openEnd] != ">") {
openEnd = openEnd + 1
}
if (openEnd >= result.count) break
var closeStart = findCloseTag_(result, openEnd + 1, "blockquote")
if (closeStart < 0) break
var content = result[openEnd + 1...closeStart].trim()
var lines = content.split("\n")
var quoted = []
for (line in lines) {
var trimmed = line.trim()
if (trimmed.count > 0) {
quoted.add("> " + trimmed)
}
}
var closeEnd = closeStart
while (closeEnd < result.count && result[closeEnd] != ">") {
closeEnd = closeEnd + 1
}
var before = result[0...start]
var after = result[closeEnd + 1..-1]
result = before + "\n" + quoted.join("\n") + "\n" + after
}
return result
}
static processListsFromHtml_(html) {
var result = html
result = processListType_(result, "ul", "-")
result = processListType_(result, "ol", "1.")
return result
}
static processListType_(html, tag, marker) {
var result = html
while (true) {
var start = findTagStart_(result, tag)
if (start < 0) break
var openEnd = start
while (openEnd < result.count && result[openEnd] != ">") {
openEnd = openEnd + 1
}
if (openEnd >= result.count) break
var closeStart = findCloseTag_(result, openEnd + 1, tag)
if (closeStart < 0) break
var content = result[openEnd + 1...closeStart]
var items = extractListItems_(content)
var mdItems = []
var num = 1
for (item in items) {
if (tag == "ol") {
mdItems.add(num.toString + ". " + item.trim())
num = num + 1
} else {
mdItems.add(marker + " " + item.trim())
}
}
var closeEnd = closeStart
while (closeEnd < result.count && result[closeEnd] != ">") {
closeEnd = closeEnd + 1
}
var before = result[0...start]
var after = result[closeEnd + 1..-1]
result = before + "\n" + mdItems.join("\n") + "\n" + after
}
return result
}
static extractListItems_(html) {
var items = []
var i = 0
while (i < html.count) {
var liStart = findTagStartFrom_(html, i, "li")
if (liStart < 0) break
var openEnd = liStart
while (openEnd < html.count && html[openEnd] != ">") {
openEnd = openEnd + 1
}
if (openEnd >= html.count) break
var closeStart = findCloseTag_(html, openEnd + 1, "li")
var content = ""
if (closeStart >= 0) {
content = html[openEnd + 1...closeStart]
var closeEnd = closeStart
while (closeEnd < html.count && html[closeEnd] != ">") {
closeEnd = closeEnd + 1
}
i = closeEnd + 1
} else {
var nextLi = findTagStartFrom_(html, openEnd + 1, "li")
if (nextLi >= 0) {
content = html[openEnd + 1...nextLi]
i = nextLi
} else {
content = html[openEnd + 1..-1]
i = html.count
}
}
items.add(stripAllTags_(content).trim())
}
return items
}
static findTagStartFrom_(html, start, tag) {
var i = start
while (i < html.count) {
if (matchTagName_(html, i, tag)) return i
i = i + 1
}
return -1
}
static processHrFromHtml_(html) {
var result = html
var i = 0
while (i < result.count) {
if (matchTagName_(result, i, "hr")) {
var end = i
while (end < result.count && result[end] != ">") {
end = end + 1
}
result = result[0...i] + "\n---\n" + result[end + 1..-1]
}
i = i + 1
}
return result
}
static processParagraphsFromHtml_(html) {
var result = html
result = replaceTag_(result, "p", "", "\n\n")
return result
}
static processBrFromHtml_(html) {
var result = html
var i = 0
while (i < result.count) {
if (matchTagName_(result, i, "br")) {
var end = i
while (end < result.count && result[end] != ">") {
end = end + 1
}
result = result[0...i] + "\n" + result[end + 1..-1]
}
i = i + 1
}
return result
}
static stripContainerTags_(html) {
var result = html
var tags = ["div", "span", "section", "article", "main", "header", "footer", "nav", "html", "body"]
for (tag in tags) {
result = replaceTag_(result, tag, "", "")
}
return result
}
static processInlineFromHtml_(html) {
var result = html
result = processStrongFromHtml_(result)
result = processEmFromHtml_(result)
result = processCodeFromHtml_(result)
result = processDelFromHtml_(result)
result = processLinksFromHtml_(result)
result = processImagesFromHtml_(result)
return result
}
static processStrongFromHtml_(html) {
var result = html
result = replaceTag_(result, "strong", "**", "**")
result = replaceTag_(result, "b", "**", "**")
return result
}
static processEmFromHtml_(html) {
var result = html
result = replaceTag_(result, "em", "*", "*")
result = replaceTag_(result, "i", "*", "*")
return result
}
static processCodeFromHtml_(html) {
return replaceTag_(html, "code", "`", "`")
}
static processDelFromHtml_(html) {
var result = html
result = replaceTag_(result, "del", "~~", "~~")
result = replaceTag_(result, "s", "~~", "~~")
result = replaceTag_(result, "strike", "~~", "~~")
return result
}
static processLinksFromHtml_(html) {
var result = html
while (true) {
var start = findTagStart_(result, "a")
if (start < 0) break
var openEnd = start
while (openEnd < result.count && result[openEnd] != ">") {
openEnd = openEnd + 1
}
if (openEnd >= result.count) break
var tagContent = result[start...openEnd + 1]
var href = extractAttr_(tagContent, "href")
var closeStart = findCloseTag_(result, openEnd + 1, "a")
if (closeStart < 0) break
var linkText = result[openEnd + 1...closeStart]
var closeEnd = closeStart
while (closeEnd < result.count && result[closeEnd] != ">") {
closeEnd = closeEnd + 1
}
var before = result[0...start]
var after = result[closeEnd + 1..-1]
if (href != null && href.count > 0) {
result = before + "[" + stripAllTags_(linkText) + "](" + href + ")" + after
} else {
result = before + stripAllTags_(linkText) + after
}
}
return result
}
static processImagesFromHtml_(html) {
var result = html
var i = 0
while (i < result.count) {
if (matchTagName_(result, i, "img")) {
var end = i
while (end < result.count && result[end] != ">") {
end = end + 1
}
var tagContent = result[i...end + 1]
var src = extractAttr_(tagContent, "src")
var alt = extractAttr_(tagContent, "alt")
if (src == null) src = ""
if (alt == null) alt = ""
var mdImg = "![" + alt + "](" + src + ")"
result = result[0...i] + mdImg + result[end + 1..-1]
i = i + mdImg.count
} else {
i = i + 1
}
}
return result
}
static extractAttr_(tag, name) {
var search = name + "=\""
var start = tag.indexOf(search)
if (start < 0) {
search = name + "='"
start = tag.indexOf(search)
}
if (start < 0) return null
var valueStart = start + search.count
var quote = search[-1]
var valueEnd = valueStart
while (valueEnd < tag.count && tag[valueEnd] != quote) {
valueEnd = valueEnd + 1
}
return tag[valueStart...valueEnd]
}
static replaceTag_(html, tag, prefix, suffix) {
var result = html
while (true) {
var start = findTagStart_(result, tag)
if (start < 0) break
var openEnd = start
while (openEnd < result.count && result[openEnd] != ">") {
openEnd = openEnd + 1
}
if (openEnd >= result.count) break
if (result[openEnd - 1] == "/") {
result = result[0...start] + result[openEnd + 1..-1]
continue
}
var closeStart = findCloseTag_(result, openEnd + 1, tag)
if (closeStart < 0) {
result = result[0...start] + result[openEnd + 1..-1]
continue
}
var content = result[openEnd + 1...closeStart]
var closeEnd = closeStart
while (closeEnd < result.count && result[closeEnd] != ">") {
closeEnd = closeEnd + 1
}
var before = result[0...start]
var after = result[closeEnd + 1..-1]
result = before + prefix + content + suffix + after
}
return result
}
static findTagStart_(html, tag) {
return findTagStartFrom_(html, 0, tag)
}
static stripAllTags_(html) {
var result = ""
var inTag = false
for (c in html) {
if (c == "<") {
inTag = true
} else if (c == ">") {
inTag = false
} else if (!inTag) {
result = result + c
}
}
return result
}
static cleanupWhitespace_(text) {
var result = text
while (result.contains("\n\n\n")) {
result = result.replace("\n\n\n", "\n\n")
}
var lines = result.split("\n")
var cleaned = []
for (line in lines) {
cleaned.add(line.trim())
}
return cleaned.join("\n")
}
}

View File

@ -329,4 +329,563 @@ static const char* markdownModuleSource =
" }\n"
" return result\n"
" }\n"
"\n"
" static fromHtml(html) { fromHtml(html, {}) }\n"
"\n"
" static fromHtml(html, options) {\n"
" var stripUnknown = options.containsKey(\"stripUnknown\") ? options[\"stripUnknown\"] : true\n"
" var text = html\n"
"\n"
" text = removeHiddenTags_(text)\n"
" text = processBlockElements_(text)\n"
" text = processInlineFromHtml_(text)\n"
" text = Html.unquote(text)\n"
" text = cleanupWhitespace_(text)\n"
"\n"
" return text.trim()\n"
" }\n"
"\n"
" static removeHiddenTags_(html) {\n"
" var result = html\n"
" var tags = [\"script\", \"style\", \"head\", \"meta\", \"link\", \"noscript\"]\n"
" for (tag in tags) {\n"
" result = removeTagWithContent_(result, tag)\n"
" }\n"
" return result\n"
" }\n"
"\n"
" static removeTagWithContent_(html, tag) {\n"
" var result = html\n"
" var openTag = \"<\" + tag\n"
" var closeTag = \"</\" + tag + \">\"\n"
" while (true) {\n"
" var startLower = result.indexOf(openTag)\n"
" var startUpper = result.indexOf(\"<\" + tag[0].codePoints.toList[0].toString)\n"
" var start = -1\n"
"\n"
" var i = 0\n"
" while (i < result.count - openTag.count) {\n"
" if (matchTagName_(result, i, tag)) {\n"
" start = i\n"
" break\n"
" }\n"
" i = i + 1\n"
" }\n"
"\n"
" if (start < 0) break\n"
"\n"
" var tagEnd = start\n"
" while (tagEnd < result.count && result[tagEnd] != \">\") {\n"
" tagEnd = tagEnd + 1\n"
" }\n"
" if (tagEnd >= result.count) break\n"
"\n"
" if (result[tagEnd - 1] == \"/\") {\n"
" result = result[0...start] + result[tagEnd + 1..-1]\n"
" continue\n"
" }\n"
"\n"
" var closeStart = findCloseTag_(result, tagEnd + 1, tag)\n"
" if (closeStart < 0) {\n"
" result = result[0...start] + result[tagEnd + 1..-1]\n"
" } else {\n"
" var closeEnd = closeStart\n"
" while (closeEnd < result.count && result[closeEnd] != \">\") {\n"
" closeEnd = closeEnd + 1\n"
" }\n"
" result = result[0...start] + result[closeEnd + 1..-1]\n"
" }\n"
" }\n"
" return result\n"
" }\n"
"\n"
" static matchTagName_(html, pos, tag) {\n"
" if (pos >= html.count || html[pos] != \"<\") return false\n"
" var rest = html[pos + 1..-1].trim()\n"
" var tagLower = tag\n"
" for (i in 0...tag.count) {\n"
" if (i >= rest.count) return false\n"
" var c1 = rest[i].codePoints.toList[0]\n"
" var c2 = tag[i].codePoints.toList[0]\n"
" if (c1 >= 65 && c1 <= 90) c1 = c1 + 32\n"
" if (c2 >= 65 && c2 <= 90) c2 = c2 + 32\n"
" if (c1 != c2) return false\n"
" }\n"
" if (tag.count < rest.count) {\n"
" var next = rest[tag.count]\n"
" if (next != \" \" && next != \">\" && next != \"/\" && next != \"\\t\" && next != \"\\n\") return false\n"
" }\n"
" return true\n"
" }\n"
"\n"
" static findCloseTag_(html, start, tag) {\n"
" var i = start\n"
" while (i < html.count - tag.count - 2) {\n"
" if (html[i] == \"<\" && html[i + 1] == \"/\") {\n"
" var match = true\n"
" for (j in 0...tag.count) {\n"
" var c1 = html[i + 2 + j].codePoints.toList[0]\n"
" var c2 = tag[j].codePoints.toList[0]\n"
" if (c1 >= 65 && c1 <= 90) c1 = c1 + 32\n"
" if (c2 >= 65 && c2 <= 90) c2 = c2 + 32\n"
" if (c1 != c2) {\n"
" match = false\n"
" break\n"
" }\n"
" }\n"
" if (match) return i\n"
" }\n"
" i = i + 1\n"
" }\n"
" return -1\n"
" }\n"
"\n"
" static processBlockElements_(html) {\n"
" var result = html\n"
"\n"
" result = processHeadingsFromHtml_(result)\n"
" result = processCodeBlocksFromHtml_(result)\n"
" result = processBlockquotesFromHtml_(result)\n"
" result = processListsFromHtml_(result)\n"
" result = processHrFromHtml_(result)\n"
" result = processParagraphsFromHtml_(result)\n"
" result = processBrFromHtml_(result)\n"
" result = stripContainerTags_(result)\n"
"\n"
" return result\n"
" }\n"
"\n"
" static processHeadingsFromHtml_(html) {\n"
" var result = html\n"
" for (level in 1..6) {\n"
" var prefix = \"\"\n"
" for (i in 0...level) prefix = prefix + \"#\"\n"
" result = replaceTag_(result, \"h\" + level.toString, prefix + \" \", \"\\n\\n\")\n"
" }\n"
" return result\n"
" }\n"
"\n"
" static processCodeBlocksFromHtml_(html) {\n"
" var result = html\n"
"\n"
" while (true) {\n"
" var preStart = findTagStart_(result, \"pre\")\n"
" if (preStart < 0) break\n"
"\n"
" var preOpenEnd = preStart\n"
" while (preOpenEnd < result.count && result[preOpenEnd] != \">\") {\n"
" preOpenEnd = preOpenEnd + 1\n"
" }\n"
" if (preOpenEnd >= result.count) break\n"
"\n"
" var preCloseStart = findCloseTag_(result, preOpenEnd + 1, \"pre\")\n"
" if (preCloseStart < 0) break\n"
"\n"
" var content = result[preOpenEnd + 1...preCloseStart]\n"
"\n"
" var codeStart = findTagStart_(content, \"code\")\n"
" if (codeStart >= 0) {\n"
" var codeOpenEnd = codeStart\n"
" while (codeOpenEnd < content.count && content[codeOpenEnd] != \">\") {\n"
" codeOpenEnd = codeOpenEnd + 1\n"
" }\n"
" var codeCloseStart = findCloseTag_(content, codeOpenEnd + 1, \"code\")\n"
" if (codeCloseStart >= 0) {\n"
" content = content[codeOpenEnd + 1...codeCloseStart]\n"
" }\n"
" }\n"
"\n"
" var preCloseEnd = preCloseStart\n"
" while (preCloseEnd < result.count && result[preCloseEnd] != \">\") {\n"
" preCloseEnd = preCloseEnd + 1\n"
" }\n"
"\n"
" var before = result[0...preStart]\n"
" var after = result[preCloseEnd + 1..-1]\n"
" result = before + \"\\n```\\n\" + content.trim() + \"\\n```\\n\" + after\n"
" }\n"
"\n"
" return result\n"
" }\n"
"\n"
" static processBlockquotesFromHtml_(html) {\n"
" var result = html\n"
"\n"
" while (true) {\n"
" var start = findTagStart_(result, \"blockquote\")\n"
" if (start < 0) break\n"
"\n"
" var openEnd = start\n"
" while (openEnd < result.count && result[openEnd] != \">\") {\n"
" openEnd = openEnd + 1\n"
" }\n"
" if (openEnd >= result.count) break\n"
"\n"
" var closeStart = findCloseTag_(result, openEnd + 1, \"blockquote\")\n"
" if (closeStart < 0) break\n"
"\n"
" var content = result[openEnd + 1...closeStart].trim()\n"
" var lines = content.split(\"\\n\")\n"
" var quoted = []\n"
" for (line in lines) {\n"
" var trimmed = line.trim()\n"
" if (trimmed.count > 0) {\n"
" quoted.add(\"> \" + trimmed)\n"
" }\n"
" }\n"
"\n"
" var closeEnd = closeStart\n"
" while (closeEnd < result.count && result[closeEnd] != \">\") {\n"
" closeEnd = closeEnd + 1\n"
" }\n"
"\n"
" var before = result[0...start]\n"
" var after = result[closeEnd + 1..-1]\n"
" result = before + \"\\n\" + quoted.join(\"\\n\") + \"\\n\" + after\n"
" }\n"
"\n"
" return result\n"
" }\n"
"\n"
" static processListsFromHtml_(html) {\n"
" var result = html\n"
" result = processListType_(result, \"ul\", \"-\")\n"
" result = processListType_(result, \"ol\", \"1.\")\n"
" return result\n"
" }\n"
"\n"
" static processListType_(html, tag, marker) {\n"
" var result = html\n"
"\n"
" while (true) {\n"
" var start = findTagStart_(result, tag)\n"
" if (start < 0) break\n"
"\n"
" var openEnd = start\n"
" while (openEnd < result.count && result[openEnd] != \">\") {\n"
" openEnd = openEnd + 1\n"
" }\n"
" if (openEnd >= result.count) break\n"
"\n"
" var closeStart = findCloseTag_(result, openEnd + 1, tag)\n"
" if (closeStart < 0) break\n"
"\n"
" var content = result[openEnd + 1...closeStart]\n"
" var items = extractListItems_(content)\n"
" var mdItems = []\n"
" var num = 1\n"
" for (item in items) {\n"
" if (tag == \"ol\") {\n"
" mdItems.add(num.toString + \". \" + item.trim())\n"
" num = num + 1\n"
" } else {\n"
" mdItems.add(marker + \" \" + item.trim())\n"
" }\n"
" }\n"
"\n"
" var closeEnd = closeStart\n"
" while (closeEnd < result.count && result[closeEnd] != \">\") {\n"
" closeEnd = closeEnd + 1\n"
" }\n"
"\n"
" var before = result[0...start]\n"
" var after = result[closeEnd + 1..-1]\n"
" result = before + \"\\n\" + mdItems.join(\"\\n\") + \"\\n\" + after\n"
" }\n"
"\n"
" return result\n"
" }\n"
"\n"
" static extractListItems_(html) {\n"
" var items = []\n"
" var i = 0\n"
" while (i < html.count) {\n"
" var liStart = findTagStartFrom_(html, i, \"li\")\n"
" if (liStart < 0) break\n"
"\n"
" var openEnd = liStart\n"
" while (openEnd < html.count && html[openEnd] != \">\") {\n"
" openEnd = openEnd + 1\n"
" }\n"
" if (openEnd >= html.count) break\n"
"\n"
" var closeStart = findCloseTag_(html, openEnd + 1, \"li\")\n"
" var content = \"\"\n"
" if (closeStart >= 0) {\n"
" content = html[openEnd + 1...closeStart]\n"
" var closeEnd = closeStart\n"
" while (closeEnd < html.count && html[closeEnd] != \">\") {\n"
" closeEnd = closeEnd + 1\n"
" }\n"
" i = closeEnd + 1\n"
" } else {\n"
" var nextLi = findTagStartFrom_(html, openEnd + 1, \"li\")\n"
" if (nextLi >= 0) {\n"
" content = html[openEnd + 1...nextLi]\n"
" i = nextLi\n"
" } else {\n"
" content = html[openEnd + 1..-1]\n"
" i = html.count\n"
" }\n"
" }\n"
" items.add(stripAllTags_(content).trim())\n"
" }\n"
" return items\n"
" }\n"
"\n"
" static findTagStartFrom_(html, start, tag) {\n"
" var i = start\n"
" while (i < html.count) {\n"
" if (matchTagName_(html, i, tag)) return i\n"
" i = i + 1\n"
" }\n"
" return -1\n"
" }\n"
"\n"
" static processHrFromHtml_(html) {\n"
" var result = html\n"
" var i = 0\n"
" while (i < result.count) {\n"
" if (matchTagName_(result, i, \"hr\")) {\n"
" var end = i\n"
" while (end < result.count && result[end] != \">\") {\n"
" end = end + 1\n"
" }\n"
" result = result[0...i] + \"\\n---\\n\" + result[end + 1..-1]\n"
" }\n"
" i = i + 1\n"
" }\n"
" return result\n"
" }\n"
"\n"
" static processParagraphsFromHtml_(html) {\n"
" var result = html\n"
" result = replaceTag_(result, \"p\", \"\", \"\\n\\n\")\n"
" return result\n"
" }\n"
"\n"
" static processBrFromHtml_(html) {\n"
" var result = html\n"
" var i = 0\n"
" while (i < result.count) {\n"
" if (matchTagName_(result, i, \"br\")) {\n"
" var end = i\n"
" while (end < result.count && result[end] != \">\") {\n"
" end = end + 1\n"
" }\n"
" result = result[0...i] + \"\\n\" + result[end + 1..-1]\n"
" }\n"
" i = i + 1\n"
" }\n"
" return result\n"
" }\n"
"\n"
" static stripContainerTags_(html) {\n"
" var result = html\n"
" var tags = [\"div\", \"span\", \"section\", \"article\", \"main\", \"header\", \"footer\", \"nav\", \"html\", \"body\"]\n"
" for (tag in tags) {\n"
" result = replaceTag_(result, tag, \"\", \"\")\n"
" }\n"
" return result\n"
" }\n"
"\n"
" static processInlineFromHtml_(html) {\n"
" var result = html\n"
"\n"
" result = processStrongFromHtml_(result)\n"
" result = processEmFromHtml_(result)\n"
" result = processCodeFromHtml_(result)\n"
" result = processDelFromHtml_(result)\n"
" result = processLinksFromHtml_(result)\n"
" result = processImagesFromHtml_(result)\n"
"\n"
" return result\n"
" }\n"
"\n"
" static processStrongFromHtml_(html) {\n"
" var result = html\n"
" result = replaceTag_(result, \"strong\", \"**\", \"**\")\n"
" result = replaceTag_(result, \"b\", \"**\", \"**\")\n"
" return result\n"
" }\n"
"\n"
" static processEmFromHtml_(html) {\n"
" var result = html\n"
" result = replaceTag_(result, \"em\", \"*\", \"*\")\n"
" result = replaceTag_(result, \"i\", \"*\", \"*\")\n"
" return result\n"
" }\n"
"\n"
" static processCodeFromHtml_(html) {\n"
" return replaceTag_(html, \"code\", \"`\", \"`\")\n"
" }\n"
"\n"
" static processDelFromHtml_(html) {\n"
" var result = html\n"
" result = replaceTag_(result, \"del\", \"~~\", \"~~\")\n"
" result = replaceTag_(result, \"s\", \"~~\", \"~~\")\n"
" result = replaceTag_(result, \"strike\", \"~~\", \"~~\")\n"
" return result\n"
" }\n"
"\n"
" static processLinksFromHtml_(html) {\n"
" var result = html\n"
"\n"
" while (true) {\n"
" var start = findTagStart_(result, \"a\")\n"
" if (start < 0) break\n"
"\n"
" var openEnd = start\n"
" while (openEnd < result.count && result[openEnd] != \">\") {\n"
" openEnd = openEnd + 1\n"
" }\n"
" if (openEnd >= result.count) break\n"
"\n"
" var tagContent = result[start...openEnd + 1]\n"
" var href = extractAttr_(tagContent, \"href\")\n"
"\n"
" var closeStart = findCloseTag_(result, openEnd + 1, \"a\")\n"
" if (closeStart < 0) break\n"
"\n"
" var linkText = result[openEnd + 1...closeStart]\n"
"\n"
" var closeEnd = closeStart\n"
" while (closeEnd < result.count && result[closeEnd] != \">\") {\n"
" closeEnd = closeEnd + 1\n"
" }\n"
"\n"
" var before = result[0...start]\n"
" var after = result[closeEnd + 1..-1]\n"
"\n"
" if (href != null && href.count > 0) {\n"
" result = before + \"[\" + stripAllTags_(linkText) + \"](\" + href + \")\" + after\n"
" } else {\n"
" result = before + stripAllTags_(linkText) + after\n"
" }\n"
" }\n"
"\n"
" return result\n"
" }\n"
"\n"
" static processImagesFromHtml_(html) {\n"
" var result = html\n"
" var i = 0\n"
"\n"
" while (i < result.count) {\n"
" if (matchTagName_(result, i, \"img\")) {\n"
" var end = i\n"
" while (end < result.count && result[end] != \">\") {\n"
" end = end + 1\n"
" }\n"
"\n"
" var tagContent = result[i...end + 1]\n"
" var src = extractAttr_(tagContent, \"src\")\n"
" var alt = extractAttr_(tagContent, \"alt\")\n"
"\n"
" if (src == null) src = \"\"\n"
" if (alt == null) alt = \"\"\n"
"\n"
" var mdImg = \"![\" + alt + \"](\" + src + \")\"\n"
" result = result[0...i] + mdImg + result[end + 1..-1]\n"
" i = i + mdImg.count\n"
" } else {\n"
" i = i + 1\n"
" }\n"
" }\n"
"\n"
" return result\n"
" }\n"
"\n"
" static extractAttr_(tag, name) {\n"
" var search = name + \"=\\\"\"\n"
" var start = tag.indexOf(search)\n"
" if (start < 0) {\n"
" search = name + \"='\"\n"
" start = tag.indexOf(search)\n"
" }\n"
" if (start < 0) return null\n"
"\n"
" var valueStart = start + search.count\n"
" var quote = search[-1]\n"
" var valueEnd = valueStart\n"
" while (valueEnd < tag.count && tag[valueEnd] != quote) {\n"
" valueEnd = valueEnd + 1\n"
" }\n"
"\n"
" return tag[valueStart...valueEnd]\n"
" }\n"
"\n"
" static replaceTag_(html, tag, prefix, suffix) {\n"
" var result = html\n"
"\n"
" while (true) {\n"
" var start = findTagStart_(result, tag)\n"
" if (start < 0) break\n"
"\n"
" var openEnd = start\n"
" while (openEnd < result.count && result[openEnd] != \">\") {\n"
" openEnd = openEnd + 1\n"
" }\n"
" if (openEnd >= result.count) break\n"
"\n"
" if (result[openEnd - 1] == \"/\") {\n"
" result = result[0...start] + result[openEnd + 1..-1]\n"
" continue\n"
" }\n"
"\n"
" var closeStart = findCloseTag_(result, openEnd + 1, tag)\n"
" if (closeStart < 0) {\n"
" result = result[0...start] + result[openEnd + 1..-1]\n"
" continue\n"
" }\n"
"\n"
" var content = result[openEnd + 1...closeStart]\n"
"\n"
" var closeEnd = closeStart\n"
" while (closeEnd < result.count && result[closeEnd] != \">\") {\n"
" closeEnd = closeEnd + 1\n"
" }\n"
"\n"
" var before = result[0...start]\n"
" var after = result[closeEnd + 1..-1]\n"
" result = before + prefix + content + suffix + after\n"
" }\n"
"\n"
" return result\n"
" }\n"
"\n"
" static findTagStart_(html, tag) {\n"
" return findTagStartFrom_(html, 0, tag)\n"
" }\n"
"\n"
" static stripAllTags_(html) {\n"
" var result = \"\"\n"
" var inTag = false\n"
" for (c in html) {\n"
" if (c == \"<\") {\n"
" inTag = true\n"
" } else if (c == \">\") {\n"
" inTag = false\n"
" } else if (!inTag) {\n"
" result = result + c\n"
" }\n"
" }\n"
" return result\n"
" }\n"
"\n"
" static cleanupWhitespace_(text) {\n"
" var result = text\n"
"\n"
" while (result.contains(\"\\n\\n\\n\")) {\n"
" result = result.replace(\"\\n\\n\\n\", \"\\n\\n\")\n"
" }\n"
"\n"
" var lines = result.split(\"\\n\")\n"
" var cleaned = []\n"
" for (line in lines) {\n"
" cleaned.add(line.trim())\n"
" }\n"
"\n"
" return cleaned.join(\"\\n\")\n"
" }\n"
"}\n";

697
src/module/pathlib.c Normal file
View File

@ -0,0 +1,697 @@
// retoor <retoor@molodetz.nl>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#ifndef _WIN32
#include <pwd.h>
#include <grp.h>
#endif
#include "uv.h"
#include "pathlib.h"
#include "scheduler.h"
#include "stat.h"
#include "vm.h"
#include "wren.h"
typedef struct {
WrenHandle* fiber;
char* path;
char* path2;
int mode;
bool recursive;
char* pattern;
} PathlibRequest;
static WrenHandle* statClass = NULL;
static uv_fs_t* createPathlibRequest(WrenHandle* fiber)
{
uv_fs_t* request = (uv_fs_t*)malloc(sizeof(uv_fs_t));
if (request == NULL) return NULL;
PathlibRequest* data = (PathlibRequest*)malloc(sizeof(PathlibRequest));
if (data == NULL) {
free(request);
return NULL;
}
data->fiber = fiber;
data->path = NULL;
data->path2 = NULL;
data->mode = 0;
data->recursive = false;
data->pattern = NULL;
request->data = data;
return request;
}
static WrenHandle* freePathlibRequest(uv_fs_t* request)
{
PathlibRequest* data = (PathlibRequest*)request->data;
WrenHandle* fiber = data->fiber;
if (data->path != NULL) free(data->path);
if (data->path2 != NULL) free(data->path2);
if (data->pattern != NULL) free(data->pattern);
free(data);
uv_fs_req_cleanup(request);
free(request);
return fiber;
}
static bool handlePathlibError(uv_fs_t* request)
{
if (request->result >= 0) return false;
PathlibRequest* data = (PathlibRequest*)request->data;
WrenHandle* fiber = data->fiber;
int error = (int)request->result;
if (data->path != NULL) free(data->path);
if (data->path2 != NULL) free(data->path2);
if (data->pattern != NULL) free(data->pattern);
free(data);
uv_fs_req_cleanup(request);
free(request);
schedulerResumeError(fiber, uv_strerror(error));
return true;
}
static void isSymlinkCallback(uv_fs_t* request)
{
if (handlePathlibError(request)) return;
bool isSymlink = S_ISLNK(request->statbuf.st_mode);
schedulerResume(freePathlibRequest(request), true);
wrenSetSlotBool(getVM(), 2, isSymlink);
schedulerFinishResume();
}
void pathlibIsSymlink(WrenVM* vm)
{
const char* path = wrenGetSlotString(vm, 1);
WrenHandle* fiber = wrenGetSlotHandle(vm, 2);
uv_fs_t* request = createPathlibRequest(fiber);
if (request == NULL) {
schedulerResumeError(fiber, "Memory allocation failed");
return;
}
uv_fs_lstat(getLoop(), request, path, isSymlinkCallback);
}
static void lstatCallback(uv_fs_t* request)
{
if (handlePathlibError(request)) return;
WrenVM* vm = getVM();
wrenEnsureSlots(vm, 3);
if (statClass == NULL) {
wrenGetVariable(vm, "io", "Stat", 0);
statClass = wrenGetSlotHandle(vm, 0);
}
wrenSetSlotHandle(vm, 2, statClass);
wrenSetSlotNewForeign(vm, 2, 2, sizeof(uv_stat_t));
uv_stat_t* data = (uv_stat_t*)wrenGetSlotForeign(vm, 2);
*data = request->statbuf;
schedulerResume(freePathlibRequest(request), true);
schedulerFinishResume();
}
void pathlibLstat(WrenVM* vm)
{
const char* path = wrenGetSlotString(vm, 1);
WrenHandle* fiber = wrenGetSlotHandle(vm, 2);
uv_fs_t* request = createPathlibRequest(fiber);
if (request == NULL) {
schedulerResumeError(fiber, "Memory allocation failed");
return;
}
uv_fs_lstat(getLoop(), request, path, lstatCallback);
}
static void touchCallback(uv_fs_t* request)
{
if (handlePathlibError(request)) return;
schedulerResume(freePathlibRequest(request), false);
}
void pathlibTouch(WrenVM* vm)
{
const char* path = wrenGetSlotString(vm, 1);
WrenHandle* fiber = wrenGetSlotHandle(vm, 2);
uv_fs_t* request = createPathlibRequest(fiber);
if (request == NULL) {
schedulerResumeError(fiber, "Memory allocation failed");
return;
}
double now = (double)time(NULL);
uv_fs_utime(getLoop(), request, path, now, now, touchCallback);
}
static void renameCallback(uv_fs_t* request)
{
if (handlePathlibError(request)) return;
schedulerResume(freePathlibRequest(request), false);
}
void pathlibRename(WrenVM* vm)
{
const char* oldPath = wrenGetSlotString(vm, 1);
const char* newPath = wrenGetSlotString(vm, 2);
WrenHandle* fiber = wrenGetSlotHandle(vm, 3);
uv_fs_t* request = createPathlibRequest(fiber);
if (request == NULL) {
schedulerResumeError(fiber, "Memory allocation failed");
return;
}
uv_fs_rename(getLoop(), request, oldPath, newPath, renameCallback);
}
static void chmodCallback(uv_fs_t* request)
{
if (handlePathlibError(request)) return;
schedulerResume(freePathlibRequest(request), false);
}
void pathlibChmod(WrenVM* vm)
{
const char* path = wrenGetSlotString(vm, 1);
int mode = (int)wrenGetSlotDouble(vm, 2);
WrenHandle* fiber = wrenGetSlotHandle(vm, 3);
uv_fs_t* request = createPathlibRequest(fiber);
if (request == NULL) {
schedulerResumeError(fiber, "Memory allocation failed");
return;
}
uv_fs_chmod(getLoop(), request, path, mode, chmodCallback);
}
static void symlinkCallback(uv_fs_t* request)
{
if (handlePathlibError(request)) return;
schedulerResume(freePathlibRequest(request), false);
}
void pathlibSymlinkTo(WrenVM* vm)
{
const char* linkPath = wrenGetSlotString(vm, 1);
const char* targetPath = wrenGetSlotString(vm, 2);
WrenHandle* fiber = wrenGetSlotHandle(vm, 3);
uv_fs_t* request = createPathlibRequest(fiber);
if (request == NULL) {
schedulerResumeError(fiber, "Memory allocation failed");
return;
}
uv_fs_symlink(getLoop(), request, targetPath, linkPath, 0, symlinkCallback);
}
static void hardlinkCallback(uv_fs_t* request)
{
if (handlePathlibError(request)) return;
schedulerResume(freePathlibRequest(request), false);
}
void pathlibHardlinkTo(WrenVM* vm)
{
const char* linkPath = wrenGetSlotString(vm, 1);
const char* targetPath = wrenGetSlotString(vm, 2);
WrenHandle* fiber = wrenGetSlotHandle(vm, 3);
uv_fs_t* request = createPathlibRequest(fiber);
if (request == NULL) {
schedulerResumeError(fiber, "Memory allocation failed");
return;
}
uv_fs_link(getLoop(), request, targetPath, linkPath, hardlinkCallback);
}
static void readlinkCallback(uv_fs_t* request)
{
if (handlePathlibError(request)) return;
WrenVM* vm = getVM();
wrenEnsureSlots(vm, 3);
wrenSetSlotString(vm, 2, (const char*)request->ptr);
schedulerResume(freePathlibRequest(request), true);
schedulerFinishResume();
}
void pathlibReadlink(WrenVM* vm)
{
const char* path = wrenGetSlotString(vm, 1);
WrenHandle* fiber = wrenGetSlotHandle(vm, 2);
uv_fs_t* request = createPathlibRequest(fiber);
if (request == NULL) {
schedulerResumeError(fiber, "Memory allocation failed");
return;
}
uv_fs_readlink(getLoop(), request, path, readlinkCallback);
}
typedef struct {
WrenHandle* fiber;
char* path1;
char* path2;
uv_stat_t stat1;
bool hasStat1;
} SamefileRequest;
static void samefileStat2Callback(uv_fs_t* request)
{
SamefileRequest* data = (SamefileRequest*)request->data;
WrenHandle* fiber = data->fiber;
if (request->result < 0) {
int error = (int)request->result;
free(data->path1);
free(data->path2);
free(data);
uv_fs_req_cleanup(request);
free(request);
schedulerResumeError(fiber, uv_strerror(error));
return;
}
bool same = (data->stat1.st_dev == request->statbuf.st_dev &&
data->stat1.st_ino == request->statbuf.st_ino);
free(data->path1);
free(data->path2);
free(data);
uv_fs_req_cleanup(request);
free(request);
schedulerResume(fiber, true);
wrenSetSlotBool(getVM(), 2, same);
schedulerFinishResume();
}
static void samefileStat1Callback(uv_fs_t* request)
{
SamefileRequest* data = (SamefileRequest*)request->data;
WrenHandle* fiber = data->fiber;
if (request->result < 0) {
int error = (int)request->result;
free(data->path1);
free(data->path2);
free(data);
uv_fs_req_cleanup(request);
free(request);
schedulerResumeError(fiber, uv_strerror(error));
return;
}
data->stat1 = request->statbuf;
data->hasStat1 = true;
uv_fs_req_cleanup(request);
uv_fs_stat(getLoop(), request, data->path2, samefileStat2Callback);
}
void pathlibSamefile(WrenVM* vm)
{
const char* path1 = wrenGetSlotString(vm, 1);
const char* path2 = wrenGetSlotString(vm, 2);
WrenHandle* fiber = wrenGetSlotHandle(vm, 3);
uv_fs_t* request = (uv_fs_t*)malloc(sizeof(uv_fs_t));
if (request == NULL) {
schedulerResumeError(fiber, "Memory allocation failed");
return;
}
SamefileRequest* data = (SamefileRequest*)malloc(sizeof(SamefileRequest));
if (data == NULL) {
free(request);
schedulerResumeError(fiber, "Memory allocation failed");
return;
}
size_t len1 = strlen(path1);
size_t len2 = strlen(path2);
data->path1 = (char*)malloc(len1 + 1);
data->path2 = (char*)malloc(len2 + 1);
if (data->path1 == NULL || data->path2 == NULL) {
if (data->path1 != NULL) free(data->path1);
if (data->path2 != NULL) free(data->path2);
free(data);
free(request);
schedulerResumeError(fiber, "Memory allocation failed");
return;
}
memcpy(data->path1, path1, len1 + 1);
memcpy(data->path2, path2, len2 + 1);
data->fiber = fiber;
data->hasStat1 = false;
request->data = data;
uv_fs_stat(getLoop(), request, data->path1, samefileStat1Callback);
}
typedef struct {
char** entries;
size_t count;
size_t capacity;
} PathList;
static void pathListInit(PathList* list)
{
list->entries = NULL;
list->count = 0;
list->capacity = 0;
}
static void pathListAdd(PathList* list, const char* path)
{
if (list->count >= list->capacity) {
size_t newCapacity = list->capacity == 0 ? 16 : list->capacity * 2;
char** newEntries = (char**)realloc(list->entries, newCapacity * sizeof(char*));
if (newEntries == NULL) return;
list->entries = newEntries;
list->capacity = newCapacity;
}
size_t len = strlen(path);
char* copy = (char*)malloc(len + 1);
if (copy == NULL) return;
memcpy(copy, path, len + 1);
list->entries[list->count++] = copy;
}
static void pathListFree(PathList* list)
{
for (size_t i = 0; i < list->count; i++) {
free(list->entries[i]);
}
if (list->entries != NULL) free(list->entries);
list->entries = NULL;
list->count = 0;
list->capacity = 0;
}
static bool matchGlobSimple(const char* text, const char* pattern)
{
const char* tp = text;
const char* pp = pattern;
const char* starTp = NULL;
const char* starPp = NULL;
while (*tp != '\0') {
if (*pp == '?' || *pp == *tp) {
tp++;
pp++;
} else if (*pp == '*') {
starPp = pp;
starTp = tp;
pp++;
} else if (starPp != NULL) {
pp = starPp + 1;
starTp++;
tp = starTp;
} else {
return false;
}
}
while (*pp == '*') pp++;
return *pp == '\0';
}
static void globRecursive(const char* basePath, const char* pattern, bool recursive, PathList* results)
{
uv_fs_t req;
int r = uv_fs_scandir(NULL, &req, basePath, 0, NULL);
if (r < 0) {
uv_fs_req_cleanup(&req);
return;
}
uv_dirent_t entry;
while (uv_fs_scandir_next(&req, &entry) != UV_EOF) {
size_t baseLen = strlen(basePath);
size_t nameLen = strlen(entry.name);
size_t fullLen = baseLen + 1 + nameLen;
char* fullPath = (char*)malloc(fullLen + 1);
if (fullPath == NULL) continue;
if (baseLen > 0 && basePath[baseLen - 1] == '/') {
snprintf(fullPath, fullLen + 1, "%s%s", basePath, entry.name);
} else {
snprintf(fullPath, fullLen + 1, "%s/%s", basePath, entry.name);
}
if (matchGlobSimple(entry.name, pattern)) {
pathListAdd(results, fullPath);
}
if (recursive && entry.type == UV_DIRENT_DIR) {
globRecursive(fullPath, pattern, true, results);
}
free(fullPath);
}
uv_fs_req_cleanup(&req);
}
typedef struct {
WrenHandle* fiber;
char* basePath;
char* pattern;
bool recursive;
} GlobRequest;
static void globCallback(uv_fs_t* request)
{
GlobRequest* data = (GlobRequest*)request->data;
WrenHandle* fiber = data->fiber;
if (request->result < 0) {
int error = (int)request->result;
free(data->basePath);
free(data->pattern);
free(data);
uv_fs_req_cleanup(request);
free(request);
schedulerResumeError(fiber, uv_strerror(error));
return;
}
PathList results;
pathListInit(&results);
uv_dirent_t entry;
while (uv_fs_scandir_next(request, &entry) != UV_EOF) {
size_t baseLen = strlen(data->basePath);
size_t nameLen = strlen(entry.name);
size_t fullLen = baseLen + 1 + nameLen;
char* fullPath = (char*)malloc(fullLen + 1);
if (fullPath == NULL) continue;
if (baseLen > 0 && data->basePath[baseLen - 1] == '/') {
snprintf(fullPath, fullLen + 1, "%s%s", data->basePath, entry.name);
} else {
snprintf(fullPath, fullLen + 1, "%s/%s", data->basePath, entry.name);
}
if (matchGlobSimple(entry.name, data->pattern)) {
pathListAdd(&results, fullPath);
}
if (data->recursive && entry.type == UV_DIRENT_DIR) {
globRecursive(fullPath, data->pattern, true, &results);
}
free(fullPath);
}
WrenVM* vm = getVM();
wrenEnsureSlots(vm, 3);
wrenSetSlotNewList(vm, 2);
for (size_t i = 0; i < results.count; i++) {
wrenSetSlotString(vm, 1, results.entries[i]);
wrenInsertInList(vm, 2, -1, 1);
}
pathListFree(&results);
free(data->basePath);
free(data->pattern);
free(data);
uv_fs_req_cleanup(request);
free(request);
schedulerResume(fiber, true);
schedulerFinishResume();
}
void pathlibGlob(WrenVM* vm)
{
const char* basePath = wrenGetSlotString(vm, 1);
const char* pattern = wrenGetSlotString(vm, 2);
bool recursive = wrenGetSlotBool(vm, 3);
WrenHandle* fiber = wrenGetSlotHandle(vm, 4);
uv_fs_t* request = (uv_fs_t*)malloc(sizeof(uv_fs_t));
if (request == NULL) {
schedulerResumeError(fiber, "Memory allocation failed");
return;
}
GlobRequest* data = (GlobRequest*)malloc(sizeof(GlobRequest));
if (data == NULL) {
free(request);
schedulerResumeError(fiber, "Memory allocation failed");
return;
}
size_t baseLen = strlen(basePath);
size_t patternLen = strlen(pattern);
data->basePath = (char*)malloc(baseLen + 1);
data->pattern = (char*)malloc(patternLen + 1);
if (data->basePath == NULL || data->pattern == NULL) {
if (data->basePath != NULL) free(data->basePath);
if (data->pattern != NULL) free(data->pattern);
free(data);
free(request);
schedulerResumeError(fiber, "Memory allocation failed");
return;
}
memcpy(data->basePath, basePath, baseLen + 1);
memcpy(data->pattern, pattern, patternLen + 1);
data->fiber = fiber;
data->recursive = recursive;
request->data = data;
uv_fs_scandir(getLoop(), request, basePath, 0, globCallback);
}
#ifndef _WIN32
static void ownerCallback(uv_fs_t* request)
{
if (request->result < 0) {
PathlibRequest* data = (PathlibRequest*)request->data;
WrenHandle* fiber = data->fiber;
int error = (int)request->result;
if (data->path != NULL) free(data->path);
free(data);
uv_fs_req_cleanup(request);
free(request);
schedulerResumeError(fiber, uv_strerror(error));
return;
}
struct passwd* pwd = getpwuid(request->statbuf.st_uid);
const char* name = pwd != NULL ? pwd->pw_name : "";
WrenVM* vm = getVM();
wrenEnsureSlots(vm, 3);
wrenSetSlotString(vm, 2, name);
schedulerResume(freePathlibRequest(request), true);
schedulerFinishResume();
}
#endif
void pathlibOwner(WrenVM* vm)
{
#ifndef _WIN32
const char* path = wrenGetSlotString(vm, 1);
WrenHandle* fiber = wrenGetSlotHandle(vm, 2);
uv_fs_t* request = createPathlibRequest(fiber);
if (request == NULL) {
schedulerResumeError(fiber, "Memory allocation failed");
return;
}
uv_fs_stat(getLoop(), request, path, ownerCallback);
#else
wrenSetSlotString(vm, 0, "");
#endif
}
#ifndef _WIN32
static void groupCallback(uv_fs_t* request)
{
if (request->result < 0) {
PathlibRequest* data = (PathlibRequest*)request->data;
WrenHandle* fiber = data->fiber;
int error = (int)request->result;
if (data->path != NULL) free(data->path);
free(data);
uv_fs_req_cleanup(request);
free(request);
schedulerResumeError(fiber, uv_strerror(error));
return;
}
struct group* grp = getgrgid(request->statbuf.st_gid);
const char* name = grp != NULL ? grp->gr_name : "";
WrenVM* vm = getVM();
wrenEnsureSlots(vm, 3);
wrenSetSlotString(vm, 2, name);
schedulerResume(freePathlibRequest(request), true);
schedulerFinishResume();
}
#endif
void pathlibGroup(WrenVM* vm)
{
#ifndef _WIN32
const char* path = wrenGetSlotString(vm, 1);
WrenHandle* fiber = wrenGetSlotHandle(vm, 2);
uv_fs_t* request = createPathlibRequest(fiber);
if (request == NULL) {
schedulerResumeError(fiber, "Memory allocation failed");
return;
}
uv_fs_stat(getLoop(), request, path, groupCallback);
#else
wrenSetSlotString(vm, 0, "");
#endif
}

21
src/module/pathlib.h Normal file
View File

@ -0,0 +1,21 @@
// retoor <retoor@molodetz.nl>
#ifndef pathlib_h
#define pathlib_h
#include "wren.h"
void pathlibIsSymlink(WrenVM* vm);
void pathlibLstat(WrenVM* vm);
void pathlibTouch(WrenVM* vm);
void pathlibRename(WrenVM* vm);
void pathlibChmod(WrenVM* vm);
void pathlibSymlinkTo(WrenVM* vm);
void pathlibHardlinkTo(WrenVM* vm);
void pathlibReadlink(WrenVM* vm);
void pathlibSamefile(WrenVM* vm);
void pathlibGlob(WrenVM* vm);
void pathlibOwner(WrenVM* vm);
void pathlibGroup(WrenVM* vm);
#endif

516
src/module/pathlib.wren vendored Normal file
View File

@ -0,0 +1,516 @@
// retoor <retoor@molodetz.nl>
import "io" for Directory, File, Stat
import "os" for Platform, Process
import "scheduler" for Scheduler
class PurePath {
construct new(path) {
if (path is PurePath) {
_path = path.toString
} else if (path is String) {
_path = path
} else {
Fiber.abort("Path must be a string or Path.")
}
}
toString { _path }
parts {
if (_path.isEmpty) return []
var result = []
var current = ""
var i = 0
if (_path[0] == "/") {
result.add("/")
i = 1
}
while (i < _path.count) {
var c = _path[i]
if (c == "/") {
if (!current.isEmpty) {
result.add(current)
current = ""
}
} else {
current = current + c
}
i = i + 1
}
if (!current.isEmpty) {
result.add(current)
}
return result
}
name {
if (_path.isEmpty) return ""
var p = parts
if (p.isEmpty) return ""
return p[-1]
}
suffix {
var n = name
if (n.isEmpty) return ""
var dotIndex = -1
for (i in (n.count - 1)..0) {
if (n[i] == ".") {
dotIndex = i
break
}
}
if (dotIndex <= 0) return ""
return n[dotIndex..-1]
}
suffixes {
var n = name
if (n.isEmpty) return []
var result = []
var i = n.count - 1
while (i > 0) {
if (n[i] == ".") {
result.insert(0, n[i..-1])
n = n[0...i]
i = n.count - 1
} else {
i = i - 1
}
}
return result
}
stem {
var n = name
if (n.isEmpty) return ""
var s = suffix
if (s.isEmpty) return n
return n[0...(n.count - s.count)]
}
parent {
if (_path.isEmpty) return Path.new(".")
var p = parts
if (p.count <= 1) {
if (isAbsolute) return Path.new("/")
return Path.new(".")
}
p = p[0...-1]
return Path.new(joinParts_(p))
}
parents {
var result = []
var current = this
while (true) {
var p = current.parent
if (p.toString == current.toString) break
result.add(p)
current = p
}
return result
}
drive {
if (_path.count >= 2 && _path[1] == ":") {
return _path[0..1]
}
return ""
}
root {
if (_path.isEmpty) return ""
if (_path[0] == "/") return "/"
if (drive != "" && _path.count >= 3 && _path[2] == "/") return "/"
return ""
}
anchor { drive + root }
isAbsolute { root != "" }
asPosix {
var result = ""
for (i in 0..._path.count) {
var c = _path[i]
if (c == "\\") {
result = result + "/"
} else {
result = result + c
}
}
return result
}
joinpath(other) {
if (other is List) {
var result = this
for (p in other) {
result = result.joinpath(p)
}
return result
}
var otherPath = other is PurePath ? other.toString : other.toString
if (otherPath.isEmpty) return Path.new(_path)
if (otherPath[0] == "/") return Path.new(otherPath)
if (_path.isEmpty) return Path.new(otherPath)
if (_path[-1] == "/") return Path.new(_path + otherPath)
return Path.new(_path + "/" + otherPath)
}
/(other) { joinpath(other) }
withName(newName) {
if (name.isEmpty) Fiber.abort("Path has no name.")
var p = parent
return p / newName
}
withStem(newStem) {
var s = suffix
return withName(newStem + s)
}
withSuffix(newSuffix) {
var s = stem
if (s.isEmpty) Fiber.abort("Path has no stem.")
return withName(s + newSuffix)
}
relativeTo(base) {
var basePath = base is PurePath ? base : PurePath.new(base)
var myParts = parts
var baseParts = basePath.parts
if (baseParts.count > myParts.count) {
Fiber.abort("'%(this)' is not relative to '%(basePath)'")
}
for (i in 0...baseParts.count) {
if (myParts[i] != baseParts[i]) {
Fiber.abort("'%(this)' is not relative to '%(basePath)'")
}
}
if (baseParts.count == myParts.count) return Path.new(".")
var relParts = myParts[baseParts.count..-1]
return Path.new(joinParts_(relParts))
}
match(pattern) {
var n = name
return matchGlob_(n, pattern)
}
==(other) {
if (!(other is PurePath)) return false
return _path == other.toString
}
!=(other) { !(this == other) }
hashCode { _path.hashCode }
joinParts_(p) {
if (p.isEmpty) return ""
if (p.count == 1 && p[0] == "/") return "/"
var result = ""
for (i in 0...p.count) {
if (i > 0 && p[i] != "/") {
if (result != "" && result[-1] != "/") {
result = result + "/"
}
}
result = result + p[i]
}
return result
}
matchGlob_(text, pattern) {
var ti = 0
var pi = 0
var starTi = -1
var starPi = -1
while (ti < text.count) {
if (pi < pattern.count && (pattern[pi] == "?" || pattern[pi] == text[ti])) {
ti = ti + 1
pi = pi + 1
} else if (pi < pattern.count && pattern[pi] == "*") {
starPi = pi
starTi = ti
pi = pi + 1
} else if (starPi >= 0) {
pi = starPi + 1
starTi = starTi + 1
ti = starTi
} else {
return false
}
}
while (pi < pattern.count && pattern[pi] == "*") {
pi = pi + 1
}
return pi == pattern.count
}
}
class Path is PurePath {
construct new(path) {
super(path)
}
static cwd { Path.new(Process.cwd) }
static home { Path.new(Platform.homePath) }
exists() {
var stat
Fiber.new {
stat = Stat.path(toString)
}.try()
return stat != null
}
isFile() {
var stat
Fiber.new {
stat = Stat.path(toString)
}.try()
if (stat == null) return false
return stat.isFile
}
isDir() {
var stat
Fiber.new {
stat = Stat.path(toString)
}.try()
if (stat == null) return false
return stat.isDirectory
}
foreign static isSymlink_(path, fiber)
isSymlink() {
return Scheduler.await_ { Path.isSymlink_(toString, Fiber.current) }
}
stat() { Stat.path(toString) }
foreign static lstat_(path, fiber)
lstat() {
return Scheduler.await_ { Path.lstat_(toString, Fiber.current) }
}
readText() { File.read(toString) }
readBytes() { File.read(toString) }
writeText(content) {
File.create(toString) {|f|
f.writeBytes(content, 0)
}
}
writeBytes(content) {
File.create(toString) {|f|
f.writeBytes(content, 0)
}
}
unlink() { File.delete(toString) }
rmdir() { Directory.delete(toString) }
mkdir() { mkdir(false) }
mkdir(parents) {
if (parents) {
var current = Path.new("")
for (part in parts) {
current = current / part
if (!current.exists()) {
Directory.create(current.toString)
}
}
} else {
Directory.create(toString)
}
}
iterdir() {
var entries = Directory.list(toString)
var result = []
for (entry in entries) {
result.add(this / entry)
}
return result
}
resolve() {
var realPath = File.realPath(toString)
return Path.new(realPath)
}
expanduser() {
var s = toString
if (s.isEmpty) return this
if (s[0] == "~") {
if (s.count == 1) {
return Path.home
}
if (s[1] == "/") {
return Path.home / s[2..-1]
}
}
return this
}
foreign static touch_(path, fiber)
touch() {
if (!exists()) {
File.create(toString) {|f| }
} else {
Scheduler.await_ { Path.touch_(toString, Fiber.current) }
}
}
foreign static rename_(oldPath, newPath, fiber)
rename(target) {
var targetPath = target is Path ? target.toString : target.toString
Scheduler.await_ { Path.rename_(toString, targetPath, Fiber.current) }
return Path.new(targetPath)
}
replace(target) { rename(target) }
foreign static chmod_(path, mode, fiber)
chmod(mode) {
Scheduler.await_ { Path.chmod_(toString, mode, Fiber.current) }
}
foreign static symlinkTo_(link, target, fiber)
symlinkTo(target) {
var targetPath = target is Path ? target.toString : target.toString
Scheduler.await_ { Path.symlinkTo_(toString, targetPath, Fiber.current) }
}
foreign static hardlinkTo_(link, target, fiber)
hardlinkTo(target) {
var targetPath = target is Path ? target.toString : target.toString
Scheduler.await_ { Path.hardlinkTo_(toString, targetPath, Fiber.current) }
}
foreign static readlink_(path, fiber)
readlink() {
return Scheduler.await_ { Path.readlink_(toString, Fiber.current) }
}
foreign static samefile_(path1, path2, fiber)
samefile(other) {
var otherPath = other is Path ? other.toString : other.toString
return Scheduler.await_ { Path.samefile_(toString, otherPath, Fiber.current) }
}
foreign static glob_(basePath, pattern, recursive, fiber)
glob(pattern) {
var results = Scheduler.await_ { Path.glob_(toString, pattern, false, Fiber.current) }
var paths = []
for (r in results) {
paths.add(Path.new(r))
}
return paths
}
rglob(pattern) {
var results = Scheduler.await_ { Path.glob_(toString, pattern, true, Fiber.current) }
var paths = []
for (r in results) {
paths.add(Path.new(r))
}
return paths
}
foreign static owner_(path, fiber)
owner() {
return Scheduler.await_ { Path.owner_(toString, Fiber.current) }
}
foreign static group_(path, fiber)
group() {
return Scheduler.await_ { Path.group_(toString, Fiber.current) }
}
walk() { walk(true) }
walk(topDown) {
var results = []
walkRecursive_(this, results, topDown)
return results
}
walkRecursive_(dir, results, topDown) {
var files = []
var dirs = []
for (entry in dir.iterdir()) {
if (entry.isDir()) {
dirs.add(entry.name)
} else {
files.add(entry.name)
}
}
if (topDown) {
results.add([dir, dirs, files])
for (d in dirs) {
walkRecursive_(dir / d, results, topDown)
}
} else {
for (d in dirs) {
walkRecursive_(dir / d, results, topDown)
}
results.add([dir, dirs, files])
}
}
rmtree() {
for (entry in walk(false)) {
var root = entry[0]
var files = entry[2]
for (f in files) {
(root / f).unlink()
}
root.rmdir()
}
}
copyfile(dest) {
var content = readBytes()
var destPath = dest is Path ? dest : Path.new(dest)
destPath.writeBytes(content)
}
}

520
src/module/pathlib.wren.inc Normal file
View File

@ -0,0 +1,520 @@
// Please do not edit this file. It has been generated automatically
// from `src/module/pathlib.wren` using `util/wren_to_c_string.py`
static const char* pathlibModuleSource =
"// retoor <retoor@molodetz.nl>\n"
"\n"
"import \"io\" for Directory, File, Stat\n"
"import \"os\" for Platform, Process\n"
"import \"scheduler\" for Scheduler\n"
"\n"
"class PurePath {\n"
" construct new(path) {\n"
" if (path is PurePath) {\n"
" _path = path.toString\n"
" } else if (path is String) {\n"
" _path = path\n"
" } else {\n"
" Fiber.abort(\"Path must be a string or Path.\")\n"
" }\n"
" }\n"
"\n"
" toString { _path }\n"
"\n"
" parts {\n"
" if (_path.isEmpty) return []\n"
" var result = []\n"
" var current = \"\"\n"
" var i = 0\n"
"\n"
" if (_path[0] == \"/\") {\n"
" result.add(\"/\")\n"
" i = 1\n"
" }\n"
"\n"
" while (i < _path.count) {\n"
" var c = _path[i]\n"
" if (c == \"/\") {\n"
" if (!current.isEmpty) {\n"
" result.add(current)\n"
" current = \"\"\n"
" }\n"
" } else {\n"
" current = current + c\n"
" }\n"
" i = i + 1\n"
" }\n"
"\n"
" if (!current.isEmpty) {\n"
" result.add(current)\n"
" }\n"
"\n"
" return result\n"
" }\n"
"\n"
" name {\n"
" if (_path.isEmpty) return \"\"\n"
" var p = parts\n"
" if (p.isEmpty) return \"\"\n"
" return p[-1]\n"
" }\n"
"\n"
" suffix {\n"
" var n = name\n"
" if (n.isEmpty) return \"\"\n"
" var dotIndex = -1\n"
" for (i in (n.count - 1)..0) {\n"
" if (n[i] == \".\") {\n"
" dotIndex = i\n"
" break\n"
" }\n"
" }\n"
" if (dotIndex <= 0) return \"\"\n"
" return n[dotIndex..-1]\n"
" }\n"
"\n"
" suffixes {\n"
" var n = name\n"
" if (n.isEmpty) return []\n"
" var result = []\n"
" var i = n.count - 1\n"
" while (i > 0) {\n"
" if (n[i] == \".\") {\n"
" result.insert(0, n[i..-1])\n"
" n = n[0...i]\n"
" i = n.count - 1\n"
" } else {\n"
" i = i - 1\n"
" }\n"
" }\n"
" return result\n"
" }\n"
"\n"
" stem {\n"
" var n = name\n"
" if (n.isEmpty) return \"\"\n"
" var s = suffix\n"
" if (s.isEmpty) return n\n"
" return n[0...(n.count - s.count)]\n"
" }\n"
"\n"
" parent {\n"
" if (_path.isEmpty) return Path.new(\".\")\n"
" var p = parts\n"
" if (p.count <= 1) {\n"
" if (isAbsolute) return Path.new(\"/\")\n"
" return Path.new(\".\")\n"
" }\n"
" p = p[0...-1]\n"
" return Path.new(joinParts_(p))\n"
" }\n"
"\n"
" parents {\n"
" var result = []\n"
" var current = this\n"
" while (true) {\n"
" var p = current.parent\n"
" if (p.toString == current.toString) break\n"
" result.add(p)\n"
" current = p\n"
" }\n"
" return result\n"
" }\n"
"\n"
" drive {\n"
" if (_path.count >= 2 && _path[1] == \":\") {\n"
" return _path[0..1]\n"
" }\n"
" return \"\"\n"
" }\n"
"\n"
" root {\n"
" if (_path.isEmpty) return \"\"\n"
" if (_path[0] == \"/\") return \"/\"\n"
" if (drive != \"\" && _path.count >= 3 && _path[2] == \"/\") return \"/\"\n"
" return \"\"\n"
" }\n"
"\n"
" anchor { drive + root }\n"
"\n"
" isAbsolute { root != \"\" }\n"
"\n"
" asPosix {\n"
" var result = \"\"\n"
" for (i in 0..._path.count) {\n"
" var c = _path[i]\n"
" if (c == \"\\\\\") {\n"
" result = result + \"/\"\n"
" } else {\n"
" result = result + c\n"
" }\n"
" }\n"
" return result\n"
" }\n"
"\n"
" joinpath(other) {\n"
" if (other is List) {\n"
" var result = this\n"
" for (p in other) {\n"
" result = result.joinpath(p)\n"
" }\n"
" return result\n"
" }\n"
"\n"
" var otherPath = other is PurePath ? other.toString : other.toString\n"
"\n"
" if (otherPath.isEmpty) return Path.new(_path)\n"
" if (otherPath[0] == \"/\") return Path.new(otherPath)\n"
" if (_path.isEmpty) return Path.new(otherPath)\n"
" if (_path[-1] == \"/\") return Path.new(_path + otherPath)\n"
" return Path.new(_path + \"/\" + otherPath)\n"
" }\n"
"\n"
" /(other) { joinpath(other) }\n"
"\n"
" withName(newName) {\n"
" if (name.isEmpty) Fiber.abort(\"Path has no name.\")\n"
" var p = parent\n"
" return p / newName\n"
" }\n"
"\n"
" withStem(newStem) {\n"
" var s = suffix\n"
" return withName(newStem + s)\n"
" }\n"
"\n"
" withSuffix(newSuffix) {\n"
" var s = stem\n"
" if (s.isEmpty) Fiber.abort(\"Path has no stem.\")\n"
" return withName(s + newSuffix)\n"
" }\n"
"\n"
" relativeTo(base) {\n"
" var basePath = base is PurePath ? base : PurePath.new(base)\n"
" var myParts = parts\n"
" var baseParts = basePath.parts\n"
"\n"
" if (baseParts.count > myParts.count) {\n"
" Fiber.abort(\"'%(this)' is not relative to '%(basePath)'\")\n"
" }\n"
"\n"
" for (i in 0...baseParts.count) {\n"
" if (myParts[i] != baseParts[i]) {\n"
" Fiber.abort(\"'%(this)' is not relative to '%(basePath)'\")\n"
" }\n"
" }\n"
"\n"
" if (baseParts.count == myParts.count) return Path.new(\".\")\n"
"\n"
" var relParts = myParts[baseParts.count..-1]\n"
" return Path.new(joinParts_(relParts))\n"
" }\n"
"\n"
" match(pattern) {\n"
" var n = name\n"
" return matchGlob_(n, pattern)\n"
" }\n"
"\n"
" ==(other) {\n"
" if (!(other is PurePath)) return false\n"
" return _path == other.toString\n"
" }\n"
"\n"
" !=(other) { !(this == other) }\n"
"\n"
" hashCode { _path.hashCode }\n"
"\n"
" joinParts_(p) {\n"
" if (p.isEmpty) return \"\"\n"
" if (p.count == 1 && p[0] == \"/\") return \"/\"\n"
" var result = \"\"\n"
" for (i in 0...p.count) {\n"
" if (i > 0 && p[i] != \"/\") {\n"
" if (result != \"\" && result[-1] != \"/\") {\n"
" result = result + \"/\"\n"
" }\n"
" }\n"
" result = result + p[i]\n"
" }\n"
" return result\n"
" }\n"
"\n"
" matchGlob_(text, pattern) {\n"
" var ti = 0\n"
" var pi = 0\n"
" var starTi = -1\n"
" var starPi = -1\n"
"\n"
" while (ti < text.count) {\n"
" if (pi < pattern.count && (pattern[pi] == \"?\" || pattern[pi] == text[ti])) {\n"
" ti = ti + 1\n"
" pi = pi + 1\n"
" } else if (pi < pattern.count && pattern[pi] == \"*\") {\n"
" starPi = pi\n"
" starTi = ti\n"
" pi = pi + 1\n"
" } else if (starPi >= 0) {\n"
" pi = starPi + 1\n"
" starTi = starTi + 1\n"
" ti = starTi\n"
" } else {\n"
" return false\n"
" }\n"
" }\n"
"\n"
" while (pi < pattern.count && pattern[pi] == \"*\") {\n"
" pi = pi + 1\n"
" }\n"
"\n"
" return pi == pattern.count\n"
" }\n"
"}\n"
"\n"
"class Path is PurePath {\n"
" construct new(path) {\n"
" super(path)\n"
" }\n"
"\n"
" static cwd { Path.new(Process.cwd) }\n"
"\n"
" static home { Path.new(Platform.homePath) }\n"
"\n"
" exists() {\n"
" var stat\n"
" Fiber.new {\n"
" stat = Stat.path(toString)\n"
" }.try()\n"
" return stat != null\n"
" }\n"
"\n"
" isFile() {\n"
" var stat\n"
" Fiber.new {\n"
" stat = Stat.path(toString)\n"
" }.try()\n"
" if (stat == null) return false\n"
" return stat.isFile\n"
" }\n"
"\n"
" isDir() {\n"
" var stat\n"
" Fiber.new {\n"
" stat = Stat.path(toString)\n"
" }.try()\n"
" if (stat == null) return false\n"
" return stat.isDirectory\n"
" }\n"
"\n"
" foreign static isSymlink_(path, fiber)\n"
"\n"
" isSymlink() {\n"
" return Scheduler.await_ { Path.isSymlink_(toString, Fiber.current) }\n"
" }\n"
"\n"
" stat() { Stat.path(toString) }\n"
"\n"
" foreign static lstat_(path, fiber)\n"
"\n"
" lstat() {\n"
" return Scheduler.await_ { Path.lstat_(toString, Fiber.current) }\n"
" }\n"
"\n"
" readText() { File.read(toString) }\n"
"\n"
" readBytes() { File.read(toString) }\n"
"\n"
" writeText(content) {\n"
" File.create(toString) {|f|\n"
" f.writeBytes(content, 0)\n"
" }\n"
" }\n"
"\n"
" writeBytes(content) {\n"
" File.create(toString) {|f|\n"
" f.writeBytes(content, 0)\n"
" }\n"
" }\n"
"\n"
" unlink() { File.delete(toString) }\n"
"\n"
" rmdir() { Directory.delete(toString) }\n"
"\n"
" mkdir() { mkdir(false) }\n"
"\n"
" mkdir(parents) {\n"
" if (parents) {\n"
" var current = Path.new(\"\")\n"
" for (part in parts) {\n"
" current = current / part\n"
" if (!current.exists()) {\n"
" Directory.create(current.toString)\n"
" }\n"
" }\n"
" } else {\n"
" Directory.create(toString)\n"
" }\n"
" }\n"
"\n"
" iterdir() {\n"
" var entries = Directory.list(toString)\n"
" var result = []\n"
" for (entry in entries) {\n"
" result.add(this / entry)\n"
" }\n"
" return result\n"
" }\n"
"\n"
" resolve() {\n"
" var realPath = File.realPath(toString)\n"
" return Path.new(realPath)\n"
" }\n"
"\n"
" expanduser() {\n"
" var s = toString\n"
" if (s.isEmpty) return this\n"
" if (s[0] == \"~\") {\n"
" if (s.count == 1) {\n"
" return Path.home\n"
" }\n"
" if (s[1] == \"/\") {\n"
" return Path.home / s[2..-1]\n"
" }\n"
" }\n"
" return this\n"
" }\n"
"\n"
" foreign static touch_(path, fiber)\n"
"\n"
" touch() {\n"
" if (!exists()) {\n"
" File.create(toString) {|f| }\n"
" } else {\n"
" Scheduler.await_ { Path.touch_(toString, Fiber.current) }\n"
" }\n"
" }\n"
"\n"
" foreign static rename_(oldPath, newPath, fiber)\n"
"\n"
" rename(target) {\n"
" var targetPath = target is Path ? target.toString : target.toString\n"
" Scheduler.await_ { Path.rename_(toString, targetPath, Fiber.current) }\n"
" return Path.new(targetPath)\n"
" }\n"
"\n"
" replace(target) { rename(target) }\n"
"\n"
" foreign static chmod_(path, mode, fiber)\n"
"\n"
" chmod(mode) {\n"
" Scheduler.await_ { Path.chmod_(toString, mode, Fiber.current) }\n"
" }\n"
"\n"
" foreign static symlinkTo_(link, target, fiber)\n"
"\n"
" symlinkTo(target) {\n"
" var targetPath = target is Path ? target.toString : target.toString\n"
" Scheduler.await_ { Path.symlinkTo_(toString, targetPath, Fiber.current) }\n"
" }\n"
"\n"
" foreign static hardlinkTo_(link, target, fiber)\n"
"\n"
" hardlinkTo(target) {\n"
" var targetPath = target is Path ? target.toString : target.toString\n"
" Scheduler.await_ { Path.hardlinkTo_(toString, targetPath, Fiber.current) }\n"
" }\n"
"\n"
" foreign static readlink_(path, fiber)\n"
"\n"
" readlink() {\n"
" return Scheduler.await_ { Path.readlink_(toString, Fiber.current) }\n"
" }\n"
"\n"
" foreign static samefile_(path1, path2, fiber)\n"
"\n"
" samefile(other) {\n"
" var otherPath = other is Path ? other.toString : other.toString\n"
" return Scheduler.await_ { Path.samefile_(toString, otherPath, Fiber.current) }\n"
" }\n"
"\n"
" foreign static glob_(basePath, pattern, recursive, fiber)\n"
"\n"
" glob(pattern) {\n"
" var results = Scheduler.await_ { Path.glob_(toString, pattern, false, Fiber.current) }\n"
" var paths = []\n"
" for (r in results) {\n"
" paths.add(Path.new(r))\n"
" }\n"
" return paths\n"
" }\n"
"\n"
" rglob(pattern) {\n"
" var results = Scheduler.await_ { Path.glob_(toString, pattern, true, Fiber.current) }\n"
" var paths = []\n"
" for (r in results) {\n"
" paths.add(Path.new(r))\n"
" }\n"
" return paths\n"
" }\n"
"\n"
" foreign static owner_(path, fiber)\n"
"\n"
" owner() {\n"
" return Scheduler.await_ { Path.owner_(toString, Fiber.current) }\n"
" }\n"
"\n"
" foreign static group_(path, fiber)\n"
"\n"
" group() {\n"
" return Scheduler.await_ { Path.group_(toString, Fiber.current) }\n"
" }\n"
"\n"
" walk() { walk(true) }\n"
"\n"
" walk(topDown) {\n"
" var results = []\n"
" walkRecursive_(this, results, topDown)\n"
" return results\n"
" }\n"
"\n"
" walkRecursive_(dir, results, topDown) {\n"
" var files = []\n"
" var dirs = []\n"
"\n"
" for (entry in dir.iterdir()) {\n"
" if (entry.isDir()) {\n"
" dirs.add(entry.name)\n"
" } else {\n"
" files.add(entry.name)\n"
" }\n"
" }\n"
"\n"
" if (topDown) {\n"
" results.add([dir, dirs, files])\n"
" for (d in dirs) {\n"
" walkRecursive_(dir / d, results, topDown)\n"
" }\n"
" } else {\n"
" for (d in dirs) {\n"
" walkRecursive_(dir / d, results, topDown)\n"
" }\n"
" results.add([dir, dirs, files])\n"
" }\n"
" }\n"
"\n"
" rmtree() {\n"
" for (entry in walk(false)) {\n"
" var root = entry[0]\n"
" var files = entry[2]\n"
" for (f in files) {\n"
" (root / f).unlink()\n"
" }\n"
" root.rmdir()\n"
" }\n"
" }\n"
"\n"
" copyfile(dest) {\n"
" var content = readBytes()\n"
" var destPath = dest is Path ? dest : Path.new(dest)\n"
" destPath.writeBytes(content)\n"
" }\n"
"}\n";

450
src/module/strutil.c Normal file
View File

@ -0,0 +1,450 @@
// retoor <retoor@molodetz.nl>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "strutil.h"
#include "wren.h"
void strutilToLower(WrenVM* vm) {
const char* str = wrenGetSlotString(vm, 1);
if (str == NULL) {
wrenSetSlotString(vm, 0, "");
return;
}
size_t len = strlen(str);
char* result = (char*)malloc(len + 1);
if (result == NULL) {
wrenSetSlotString(vm, 0, "");
return;
}
for (size_t i = 0; i < len; i++) {
unsigned char c = (unsigned char)str[i];
if (c >= 'A' && c <= 'Z') {
result[i] = c + 32;
} else {
result[i] = c;
}
}
result[len] = '\0';
wrenSetSlotString(vm, 0, result);
free(result);
}
void strutilToUpper(WrenVM* vm) {
const char* str = wrenGetSlotString(vm, 1);
if (str == NULL) {
wrenSetSlotString(vm, 0, "");
return;
}
size_t len = strlen(str);
char* result = (char*)malloc(len + 1);
if (result == NULL) {
wrenSetSlotString(vm, 0, "");
return;
}
for (size_t i = 0; i < len; i++) {
unsigned char c = (unsigned char)str[i];
if (c >= 'a' && c <= 'z') {
result[i] = c - 32;
} else {
result[i] = c;
}
}
result[len] = '\0';
wrenSetSlotString(vm, 0, result);
free(result);
}
static const char hexCharsLower[] = "0123456789abcdef";
static const char hexCharsUpper[] = "0123456789ABCDEF";
void strutilHexEncode(WrenVM* vm) {
int length = 0;
const char* bytes = wrenGetSlotBytes(vm, 1, &length);
if (bytes == NULL || length == 0) {
wrenSetSlotString(vm, 0, "");
return;
}
char* result = (char*)malloc(length * 2 + 1);
if (result == NULL) {
wrenSetSlotString(vm, 0, "");
return;
}
for (int i = 0; i < length; i++) {
unsigned char b = (unsigned char)bytes[i];
result[i * 2] = hexCharsLower[(b >> 4) & 0x0F];
result[i * 2 + 1] = hexCharsLower[b & 0x0F];
}
result[length * 2] = '\0';
wrenSetSlotString(vm, 0, result);
free(result);
}
static int hexValue(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
return -1;
}
void strutilHexDecode(WrenVM* vm) {
const char* hex = wrenGetSlotString(vm, 1);
if (hex == NULL) {
wrenSetSlotBytes(vm, 0, "", 0);
return;
}
size_t len = strlen(hex);
if (len == 0 || len % 2 != 0) {
wrenSetSlotBytes(vm, 0, "", 0);
return;
}
size_t outLen = len / 2;
char* result = (char*)malloc(outLen);
if (result == NULL) {
wrenSetSlotBytes(vm, 0, "", 0);
return;
}
for (size_t i = 0; i < outLen; i++) {
int hi = hexValue(hex[i * 2]);
int lo = hexValue(hex[i * 2 + 1]);
if (hi < 0 || lo < 0) {
free(result);
wrenSetSlotBytes(vm, 0, "", 0);
return;
}
result[i] = (char)((hi << 4) | lo);
}
wrenSetSlotBytes(vm, 0, result, outLen);
free(result);
}
void strutilRepeat(WrenVM* vm) {
const char* str = wrenGetSlotString(vm, 1);
int count = (int)wrenGetSlotDouble(vm, 2);
if (str == NULL || count <= 0) {
wrenSetSlotString(vm, 0, "");
return;
}
size_t len = strlen(str);
if (len == 0) {
wrenSetSlotString(vm, 0, "");
return;
}
size_t totalLen = len * count;
char* result = (char*)malloc(totalLen + 1);
if (result == NULL) {
wrenSetSlotString(vm, 0, "");
return;
}
for (int i = 0; i < count; i++) {
memcpy(result + i * len, str, len);
}
result[totalLen] = '\0';
wrenSetSlotString(vm, 0, result);
free(result);
}
void strutilPadLeft(WrenVM* vm) {
const char* str = wrenGetSlotString(vm, 1);
int width = (int)wrenGetSlotDouble(vm, 2);
const char* padChar = wrenGetSlotString(vm, 3);
if (str == NULL) {
wrenSetSlotString(vm, 0, "");
return;
}
size_t len = strlen(str);
if (width <= 0 || (size_t)width <= len) {
wrenSetSlotString(vm, 0, str);
return;
}
char pad = (padChar != NULL && padChar[0] != '\0') ? padChar[0] : ' ';
size_t padLen = width - len;
char* result = (char*)malloc(width + 1);
if (result == NULL) {
wrenSetSlotString(vm, 0, str);
return;
}
memset(result, pad, padLen);
memcpy(result + padLen, str, len);
result[width] = '\0';
wrenSetSlotString(vm, 0, result);
free(result);
}
void strutilPadRight(WrenVM* vm) {
const char* str = wrenGetSlotString(vm, 1);
int width = (int)wrenGetSlotDouble(vm, 2);
const char* padChar = wrenGetSlotString(vm, 3);
if (str == NULL) {
wrenSetSlotString(vm, 0, "");
return;
}
size_t len = strlen(str);
if (width <= 0 || (size_t)width <= len) {
wrenSetSlotString(vm, 0, str);
return;
}
char pad = (padChar != NULL && padChar[0] != '\0') ? padChar[0] : ' ';
size_t padLen = width - len;
char* result = (char*)malloc(width + 1);
if (result == NULL) {
wrenSetSlotString(vm, 0, str);
return;
}
memcpy(result, str, len);
memset(result + len, pad, padLen);
result[width] = '\0';
wrenSetSlotString(vm, 0, result);
free(result);
}
void strutilEscapeHtml(WrenVM* vm) {
const char* str = wrenGetSlotString(vm, 1);
if (str == NULL) {
wrenSetSlotString(vm, 0, "");
return;
}
size_t len = strlen(str);
size_t outLen = 0;
for (size_t i = 0; i < len; i++) {
char c = str[i];
if (c == '&') outLen += 5;
else if (c == '<') outLen += 4;
else if (c == '>') outLen += 4;
else if (c == '"') outLen += 6;
else if (c == '\'') outLen += 5;
else outLen += 1;
}
char* result = (char*)malloc(outLen + 1);
if (result == NULL) {
wrenSetSlotString(vm, 0, str);
return;
}
size_t j = 0;
for (size_t i = 0; i < len; i++) {
char c = str[i];
if (c == '&') {
memcpy(result + j, "&amp;", 5);
j += 5;
} else if (c == '<') {
memcpy(result + j, "&lt;", 4);
j += 4;
} else if (c == '>') {
memcpy(result + j, "&gt;", 4);
j += 4;
} else if (c == '"') {
memcpy(result + j, "&quot;", 6);
j += 6;
} else if (c == '\'') {
memcpy(result + j, "&#39;", 5);
j += 5;
} else {
result[j++] = c;
}
}
result[j] = '\0';
wrenSetSlotString(vm, 0, result);
free(result);
}
void strutilEscapeJson(WrenVM* vm) {
const char* str = wrenGetSlotString(vm, 1);
if (str == NULL) {
wrenSetSlotString(vm, 0, "\"\"");
return;
}
size_t len = strlen(str);
size_t outLen = 2;
for (size_t i = 0; i < len; i++) {
unsigned char c = (unsigned char)str[i];
if (c == '"' || c == '\\' || c == '\b' || c == '\f' ||
c == '\n' || c == '\r' || c == '\t') {
outLen += 2;
} else if (c < 32) {
outLen += 6;
} else {
outLen += 1;
}
}
char* result = (char*)malloc(outLen + 1);
if (result == NULL) {
wrenSetSlotString(vm, 0, "\"\"");
return;
}
size_t j = 0;
result[j++] = '"';
for (size_t i = 0; i < len; i++) {
unsigned char c = (unsigned char)str[i];
if (c == '"') {
result[j++] = '\\';
result[j++] = '"';
} else if (c == '\\') {
result[j++] = '\\';
result[j++] = '\\';
} else if (c == '\b') {
result[j++] = '\\';
result[j++] = 'b';
} else if (c == '\f') {
result[j++] = '\\';
result[j++] = 'f';
} else if (c == '\n') {
result[j++] = '\\';
result[j++] = 'n';
} else if (c == '\r') {
result[j++] = '\\';
result[j++] = 'r';
} else if (c == '\t') {
result[j++] = '\\';
result[j++] = 't';
} else if (c < 32) {
result[j++] = '\\';
result[j++] = 'u';
result[j++] = '0';
result[j++] = '0';
result[j++] = hexCharsLower[(c >> 4) & 0x0F];
result[j++] = hexCharsLower[c & 0x0F];
} else {
result[j++] = c;
}
}
result[j++] = '"';
result[j] = '\0';
wrenSetSlotString(vm, 0, result);
free(result);
}
static int isUnreserved(unsigned char c) {
if (c >= 'A' && c <= 'Z') return 1;
if (c >= 'a' && c <= 'z') return 1;
if (c >= '0' && c <= '9') return 1;
if (c == '-' || c == '_' || c == '.' || c == '~') return 1;
return 0;
}
void strutilUrlEncode(WrenVM* vm) {
const char* str = wrenGetSlotString(vm, 1);
if (str == NULL) {
wrenSetSlotString(vm, 0, "");
return;
}
size_t len = strlen(str);
size_t outLen = 0;
for (size_t i = 0; i < len; i++) {
unsigned char c = (unsigned char)str[i];
if (isUnreserved(c)) {
outLen += 1;
} else if (c == ' ') {
outLen += 1;
} else {
outLen += 3;
}
}
char* result = (char*)malloc(outLen + 1);
if (result == NULL) {
wrenSetSlotString(vm, 0, str);
return;
}
size_t j = 0;
for (size_t i = 0; i < len; i++) {
unsigned char c = (unsigned char)str[i];
if (isUnreserved(c)) {
result[j++] = c;
} else if (c == ' ') {
result[j++] = '+';
} else {
result[j++] = '%';
result[j++] = hexCharsUpper[(c >> 4) & 0x0F];
result[j++] = hexCharsUpper[c & 0x0F];
}
}
result[j] = '\0';
wrenSetSlotString(vm, 0, result);
free(result);
}
void strutilUrlDecode(WrenVM* vm) {
const char* str = wrenGetSlotString(vm, 1);
if (str == NULL) {
wrenSetSlotString(vm, 0, "");
return;
}
size_t len = strlen(str);
char* result = (char*)malloc(len + 1);
if (result == NULL) {
wrenSetSlotString(vm, 0, str);
return;
}
size_t j = 0;
for (size_t i = 0; i < len; i++) {
char c = str[i];
if (c == '+') {
result[j++] = ' ';
} else if (c == '%' && i + 2 < len) {
int hi = hexValue(str[i + 1]);
int lo = hexValue(str[i + 2]);
if (hi >= 0 && lo >= 0) {
result[j++] = (char)((hi << 4) | lo);
i += 2;
} else {
result[j++] = c;
}
} else {
result[j++] = c;
}
}
result[j] = '\0';
wrenSetSlotString(vm, 0, result);
free(result);
}

20
src/module/strutil.h Normal file
View File

@ -0,0 +1,20 @@
// retoor <retoor@molodetz.nl>
#ifndef wren_strutil_h
#define wren_strutil_h
#include "wren.h"
void strutilToLower(WrenVM* vm);
void strutilToUpper(WrenVM* vm);
void strutilHexEncode(WrenVM* vm);
void strutilHexDecode(WrenVM* vm);
void strutilRepeat(WrenVM* vm);
void strutilPadLeft(WrenVM* vm);
void strutilPadRight(WrenVM* vm);
void strutilEscapeHtml(WrenVM* vm);
void strutilEscapeJson(WrenVM* vm);
void strutilUrlEncode(WrenVM* vm);
void strutilUrlDecode(WrenVM* vm);
#endif

15
src/module/strutil.wren vendored Normal file
View File

@ -0,0 +1,15 @@
// retoor <retoor@molodetz.nl>
class Str {
foreign static toLower(str)
foreign static toUpper(str)
foreign static hexEncode(bytes)
foreign static hexDecode(hex)
foreign static repeat(str, count)
foreign static padLeft(str, width, char)
foreign static padRight(str, width, char)
foreign static escapeHtml(str)
foreign static escapeJson(str)
foreign static urlEncode(str)
foreign static urlDecode(str)
}

View File

@ -0,0 +1,19 @@
// Please do not edit this file. It has been generated automatically
// from `src/module/strutil.wren` using `util/wren_to_c_string.py`
static const char* strutilModuleSource =
"// retoor <retoor@molodetz.nl>\n"
"\n"
"class Str {\n"
" foreign static toLower(str)\n"
" foreign static toUpper(str)\n"
" foreign static hexEncode(bytes)\n"
" foreign static hexDecode(hex)\n"
" foreign static repeat(str, count)\n"
" foreign static padLeft(str, width, char)\n"
" foreign static padRight(str, width, char)\n"
" foreign static escapeHtml(str)\n"
" foreign static escapeJson(str)\n"
" foreign static urlEncode(str)\n"
" foreign static urlDecode(str)\n"
"}\n";

17
src/module/uuid.wren vendored
View File

@ -1,22 +1,11 @@
// retoor <retoor@molodetz.nl>
import "crypto" for Crypto
import "strutil" for Str
import "bytes" for Bytes
class Uuid {
static hexDigit_(n) {
if (n < 10) return String.fromCodePoint(48 + n)
return String.fromCodePoint(97 + n - 10)
}
static toHex_(bytes) {
var hex = ""
for (b in bytes) {
var hi = (b >> 4) & 0x0F
var lo = b & 0x0F
hex = hex + hexDigit_(hi) + hexDigit_(lo)
}
return hex
}
static toHex_(bytes) { Str.hexEncode(Bytes.fromList(bytes)) }
static v4() {
var bytes = Crypto.randomBytes(16)

View File

@ -5,22 +5,11 @@ static const char* uuidModuleSource =
"// retoor <retoor@molodetz.nl>\n"
"\n"
"import \"crypto\" for Crypto\n"
"import \"strutil\" for Str\n"
"import \"bytes\" for Bytes\n"
"\n"
"class Uuid {\n"
" static hexDigit_(n) {\n"
" if (n < 10) return String.fromCodePoint(48 + n)\n"
" return String.fromCodePoint(97 + n - 10)\n"
" }\n"
"\n"
" static toHex_(bytes) {\n"
" var hex = \"\"\n"
" for (b in bytes) {\n"
" var hi = (b >> 4) & 0x0F\n"
" var lo = b & 0x0F\n"
" hex = hex + hexDigit_(hi) + hexDigit_(lo)\n"
" }\n"
" return hex\n"
" }\n"
" static toHex_(bytes) { Str.hexEncode(Bytes.fromList(bytes)) }\n"
"\n"
" static v4() {\n"
" var bytes = Crypto.randomBytes(16)\n"

14
src/module/web.wren vendored
View File

@ -9,6 +9,7 @@ import "html" for Html
import "uuid" for Uuid
import "datetime" for DateTime
import "io" for File
import "strutil" for Str
class Request {
construct new_(method, path, query, headers, body, params, socket) {
@ -77,18 +78,7 @@ class Request {
session { _session }
session=(s) { _session = s }
toLower_(str) {
var result = ""
for (c in str) {
var cp = c.codePoints.toList[0]
if (cp >= 65 && cp <= 90) {
result = result + String.fromCodePoint(cp + 32)
} else {
result = result + c
}
}
return result
}
toLower_(str) { Str.toLower(str) }
}
class Response {

View File

@ -13,6 +13,7 @@ static const char* webModuleSource =
"import \"uuid\" for Uuid\n"
"import \"datetime\" for DateTime\n"
"import \"io\" for File\n"
"import \"strutil\" for Str\n"
"\n"
"class Request {\n"
" construct new_(method, path, query, headers, body, params, socket) {\n"
@ -81,18 +82,7 @@ static const char* webModuleSource =
" session { _session }\n"
" session=(s) { _session = s }\n"
"\n"
" toLower_(str) {\n"
" var result = \"\"\n"
" for (c in str) {\n"
" var cp = c.codePoints.toList[0]\n"
" if (cp >= 65 && cp <= 90) {\n"
" result = result + String.fromCodePoint(cp + 32)\n"
" } else {\n"
" result = result + c\n"
" }\n"
" }\n"
" return result\n"
" }\n"
" toLower_(str) { Str.toLower(str) }\n"
"}\n"
"\n"
"class Response {\n"

View File

@ -6,6 +6,7 @@ import "dns" for Dns
import "crypto" for Crypto, Hash
import "base64" for Base64
import "bytes" for Bytes
import "strutil" for Str
class WebSocketMessage {
construct new_(opcode, payload, fin) {
@ -205,18 +206,7 @@ class WebSocket {
return lines
}
static toLower_(str) {
var result = ""
for (c in str) {
var cp = c.codePoints[0]
if (cp >= 65 && cp <= 90) {
result = result + String.fromCodePoint(cp + 32)
} else {
result = result + c
}
}
return result
}
static toLower_(str) { Str.toLower(str) }
static computeAcceptKey_(key) {
var magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"

View File

@ -10,6 +10,7 @@ static const char* websocketModuleSource =
"import \"crypto\" for Crypto, Hash\n"
"import \"base64\" for Base64\n"
"import \"bytes\" for Bytes\n"
"import \"strutil\" for Str\n"
"\n"
"class WebSocketMessage {\n"
" construct new_(opcode, payload, fin) {\n"
@ -209,18 +210,7 @@ static const char* websocketModuleSource =
" return lines\n"
" }\n"
"\n"
" static toLower_(str) {\n"
" var result = \"\"\n"
" for (c in str) {\n"
" var cp = c.codePoints[0]\n"
" if (cp >= 65 && cp <= 90) {\n"
" result = result + String.fromCodePoint(cp + 32)\n"
" } else {\n"
" result = result + c\n"
" }\n"
" }\n"
" return result\n"
" }\n"
" static toLower_(str) { Str.toLower(str) }\n"
"\n"
" static computeAcceptKey_(key) {\n"
" var magic = \"258EAFA5-E914-47DA-95CA-C5AB0DC85B11\"\n"

24
test/dataset/boolean_fields.wren vendored Normal file
View File

@ -0,0 +1,24 @@
// retoor <retoor@molodetz.nl>
import "dataset" for Dataset
var ds = Dataset.memory()
var users = ds["users"]
var active = users.insert({"name": "Alice", "active": true, "admin": false})
System.print(active["active"]) // expect: true
System.print(active["admin"]) // expect: false
var inactive = users.insert({"name": "Bob", "active": false, "admin": true})
System.print(inactive["active"]) // expect: false
System.print(inactive["admin"]) // expect: true
var found = users.findOne({"name": "Alice"})
System.print(found["active"]) // expect: 1
System.print(found["admin"]) // expect: 0
var activeUsers = users.find({"active": true})
System.print(activeUsers.count) // expect: 1
System.print(activeUsers[0]["name"]) // expect: Alice
ds.close()

23
test/dataset/custom_created_at.wren vendored Normal file
View File

@ -0,0 +1,23 @@
// retoor <retoor@molodetz.nl>
import "dataset" for Dataset
var ds = Dataset.memory()
var events = ds["events"]
var event = events.insert({
"name": "Conference",
"created_at": "2024-01-15T10:00:00"
})
System.print(event["created_at"]) // expect: 2024-01-15T10:00:00
System.print(event["name"]) // expect: Conference
var found = events.findOne({"name": "Conference"})
System.print(found["created_at"]) // expect: 2024-01-15T10:00:00
var auto = events.insert({"name": "Meeting"})
System.print(auto["created_at"] != null) // expect: true
System.print(auto["created_at"] != "2024-01-15T10:00:00") // expect: true
ds.close()

24
test/dataset/custom_uid.wren vendored Normal file
View File

@ -0,0 +1,24 @@
// retoor <retoor@molodetz.nl>
import "dataset" for Dataset
var ds = Dataset.memory()
var users = ds["users"]
var user = users.insert({
"uid": "my-custom-id-123",
"name": "Alice"
})
System.print(user["uid"]) // expect: my-custom-id-123
System.print(user["name"]) // expect: Alice
var found = users.findOne({"uid": "my-custom-id-123"})
System.print(found != null) // expect: true
System.print(found["name"]) // expect: Alice
users.update({"uid": "my-custom-id-123", "name": "Alice Smith"})
var updated = users.findOne({"uid": "my-custom-id-123"})
System.print(updated["name"]) // expect: Alice Smith
ds.close()

38
test/dataset/edge_cases.wren vendored Normal file
View File

@ -0,0 +1,38 @@
// retoor <retoor@molodetz.nl>
import "dataset" for Dataset
var ds = Dataset.memory()
var users = ds["users"]
var emptyAll = users.all()
System.print(emptyAll.count) // expect: 0
System.print(users.count()) // expect: 0
var user = users.insert({"name": "Alice", "score": 100})
var notFound = users.findOne({"name": "Nobody"})
System.print(notFound == null) // expect: true
var emptyFind = users.find({"name": "Nobody"})
System.print(emptyFind.count) // expect: 0
System.print(emptyFind is List) // expect: true
var uid = user["uid"]
var changes1 = users.update({"uid": uid, "score": 200})
System.print(changes1) // expect: 1
var changes2 = users.update({"uid": uid, "score": 300, "level": 5})
System.print(changes2) // expect: 1
var updated = users.findOne({"uid": uid})
System.print(updated["score"]) // expect: 300
System.print(updated["level"]) // expect: 5
var fakeChanges = users.update({"uid": "nonexistent-uid", "score": 999})
System.print(fakeChanges) // expect: 0
System.print(users.delete("nonexistent-uid")) // expect: false
ds.close()

33
test/dataset/file_database.wren vendored Normal file
View File

@ -0,0 +1,33 @@
// retoor <retoor@molodetz.nl>
import "dataset" for Dataset
import "io" for File
var dbPath = "/tmp/test_dataset.db"
if (File.exists(dbPath)) {
File.delete(dbPath)
}
var ds = Dataset.open(dbPath)
var users = ds["users"]
var user = users.insert({"name": "Alice", "score": 100})
System.print(user["name"]) // expect: Alice
System.print(users.count()) // expect: 1
ds.close()
System.print(File.exists(dbPath)) // expect: true
var ds2 = Dataset.open(dbPath)
var users2 = ds2["users"]
System.print(users2.count()) // expect: 1
var found = users2.findOne({"name": "Alice"})
System.print(found["score"]) // expect: 100
ds2.close()
File.delete(dbPath)
System.print(File.exists(dbPath)) // expect: false

29
test/dataset/hard_delete.wren vendored Normal file
View File

@ -0,0 +1,29 @@
// retoor <retoor@molodetz.nl>
import "dataset" for Dataset
var ds = Dataset.memory()
var users = ds["users"]
var user = users.insert({"name": "Alice"})
var uid = user["uid"]
System.print(users.count()) // expect: 1
users.delete(uid)
System.print(users.count()) // expect: 0
var softDeleted = ds.db.query("SELECT * FROM users WHERE uid = ?", [uid])
System.print(softDeleted.count) // expect: 1
System.print(softDeleted[0]["deleted_at"] != null) // expect: true
var user2 = users.insert({"name": "Bob"})
var uid2 = user2["uid"]
System.print(users.hardDelete(uid2)) // expect: true
var hardDeleted = ds.db.query("SELECT * FROM users WHERE uid = ?", [uid2])
System.print(hardDeleted.count) // expect: 0
System.print(users.hardDelete("nonexistent")) // expect: false
ds.close()

39
test/dataset/json_fields.wren vendored Normal file
View File

@ -0,0 +1,39 @@
// retoor <retoor@molodetz.nl>
import "dataset" for Dataset
var ds = Dataset.memory()
var posts = ds["posts"]
var post = posts.insert({
"title": "Hello",
"tags": ["wren", "programming"],
"metadata": {"views": 100, "featured": true}
})
System.print(post["title"]) // expect: Hello
System.print(post["tags"] is List) // expect: true
System.print(post["tags"].count) // expect: 2
System.print(post["tags"][0]) // expect: wren
System.print(post["metadata"] is Map) // expect: true
System.print(post["metadata"]["views"]) // expect: 100
System.print(post["metadata"]["featured"]) // expect: true
var retrieved = posts.findOne({"title": "Hello"})
System.print(retrieved["tags"] is List) // expect: true
System.print(retrieved["tags"][1]) // expect: programming
System.print(retrieved["metadata"]["views"]) // expect: 100
var nested = posts.insert({
"title": "Nested",
"data": {
"level1": {
"level2": ["a", "b", "c"]
}
}
})
var fetched = posts.findOne({"title": "Nested"})
System.print(fetched["data"]["level1"]["level2"][0]) // expect: a
ds.close()

33
test/dataset/multiple_tables.wren vendored Normal file
View File

@ -0,0 +1,33 @@
// retoor <retoor@molodetz.nl>
import "dataset" for Dataset
var ds = Dataset.memory()
var users = ds["users"]
var posts = ds["posts"]
var comments = ds["comments"]
var user = users.insert({"name": "Alice"})
var userUid = user["uid"]
var post = posts.insert({"title": "Hello", "author_uid": userUid})
var postUid = post["uid"]
comments.insert({"text": "Nice!", "post_uid": postUid, "author_uid": userUid})
comments.insert({"text": "Great!", "post_uid": postUid, "author_uid": userUid})
System.print(users.count()) // expect: 1
System.print(posts.count()) // expect: 1
System.print(comments.count()) // expect: 2
var tables = ds.tables
System.print(tables.count) // expect: 3
System.print(tables.contains("users")) // expect: true
System.print(tables.contains("posts")) // expect: true
System.print(tables.contains("comments")) // expect: true
var postComments = comments.find({"post_uid": postUid})
System.print(postComments.count) // expect: 2
ds.close()

22
test/dataset/null_operator.wren vendored Normal file
View File

@ -0,0 +1,22 @@
// retoor <retoor@molodetz.nl>
import "dataset" for Dataset
var ds = Dataset.memory()
var users = ds["users"]
users.insert({"name": "Alice", "email": "alice@example.com"})
users.insert({"name": "Bob", "email": null})
users.insert({"name": "Charlie"})
var withEmail = users.find({"email__null": false})
System.print(withEmail.count) // expect: 1
System.print(withEmail[0]["name"]) // expect: Alice
var withoutEmail = users.find({"email__null": true})
System.print(withoutEmail.count) // expect: 2
var all = users.all()
System.print(all.count) // expect: 3
ds.close()

27
test/dataset/number_types.wren vendored Normal file
View File

@ -0,0 +1,27 @@
// retoor <retoor@molodetz.nl>
import "dataset" for Dataset
var ds = Dataset.memory()
var products = ds["products"]
var p1 = products.insert({"name": "Widget", "price": 19.99, "quantity": 100})
System.print(p1["price"]) // expect: 19.99
System.print(p1["quantity"]) // expect: 100
var p2 = products.insert({"name": "Gadget", "price": 0.5, "quantity": 0})
System.print(p2["price"]) // expect: 0.5
System.print(p2["quantity"]) // expect: 0
var found = products.findOne({"name": "Widget"})
System.print(found["price"]) // expect: 19.99
System.print(found["quantity"]) // expect: 100
var cheap = products.find({"price__lt": 1})
System.print(cheap.count) // expect: 1
System.print(cheap[0]["name"]) // expect: Gadget
var expensive = products.find({"price__gte": 10})
System.print(expensive.count) // expect: 1
ds.close()

36
test/dataset/query_operators.wren vendored Normal file
View File

@ -0,0 +1,36 @@
// retoor <retoor@molodetz.nl>
import "dataset" for Dataset
var ds = Dataset.memory()
var products = ds["products"]
products.insert({"name": "Apple", "price": 10, "stock": 100})
products.insert({"name": "Banana", "price": 5, "stock": 50})
products.insert({"name": "Cherry", "price": 20, "stock": 0})
products.insert({"name": "Date", "price": 15, "stock": 25})
var gte = products.find({"price__gte": 15})
System.print(gte.count) // expect: 2
var lte = products.find({"price__lte": 10})
System.print(lte.count) // expect: 2
var ne = products.find({"name__ne": "Apple"})
System.print(ne.count) // expect: 3
var like = products.find({"name__like": "A\%"})
System.print(like.count) // expect: 1
System.print(like[0]["name"]) // expect: Apple
var inList = products.find({"name__in": ["Apple", "Banana"]})
System.print(inList.count) // expect: 2
var zeroStock = products.find({"stock": 0})
System.print(zeroStock.count) // expect: 1
System.print(zeroStock[0]["name"]) // expect: Cherry
var combined = products.find({"price__gte": 10, "stock__gt": 0})
System.print(combined.count) // expect: 2
ds.close()

16
test/dataset/table_name.wren vendored Normal file
View File

@ -0,0 +1,16 @@
// retoor <retoor@molodetz.nl>
import "dataset" for Dataset
var ds = Dataset.memory()
var users = ds["users"]
System.print(users.name) // expect: users
var products = ds["products"]
System.print(products.name) // expect: products
var orders = ds["my_orders"]
System.print(orders.name) // expect: my_orders
ds.close()

20
test/dataset/tables_property.wren vendored Normal file
View File

@ -0,0 +1,20 @@
// retoor <retoor@molodetz.nl>
import "dataset" for Dataset
var ds = Dataset.memory()
System.print(ds.tables.count) // expect: 0
ds["users"].insert({"name": "Alice"})
System.print(ds.tables.count) // expect: 1
System.print(ds.tables.contains("users")) // expect: true
ds["products"].insert({"name": "Widget"})
System.print(ds.tables.count) // expect: 2
System.print(ds.tables.contains("products")) // expect: true
ds["orders"].insert({"total": 100})
System.print(ds.tables.count) // expect: 3
ds.close()

22
test/markdown/blocks_advanced.wren vendored Normal file
View File

@ -0,0 +1,22 @@
// retoor <retoor@molodetz.nl>
import "markdown" for Markdown
var multiLine = Markdown.toHtml("```\nline 1\nline 2\nline 3\n```")
System.print(multiLine.contains("line 1")) // expect: true
System.print(multiLine.contains("line 2")) // expect: true
System.print(multiLine.contains("line 3")) // expect: true
System.print(multiLine.contains("<pre><code>")) // expect: true
var multiQuote = Markdown.toHtml("> line 1\n> line 2")
System.print(multiQuote.contains("<blockquote>")) // expect: true
System.print(multiQuote.contains("line 1")) // expect: true
System.print(multiQuote.contains("line 2")) // expect: true
var withLang = Markdown.toHtml("```wren\nSystem.print(\"hello\")\n```")
System.print(withLang.contains("<pre><code>")) // expect: true
System.print(withLang.contains("System.print")) // expect: true
var emptyBlock = Markdown.toHtml("```\n\n```")
System.print(emptyBlock.contains("<pre><code>")) // expect: true
System.print(emptyBlock.contains("</code></pre>")) // expect: true

24
test/markdown/formatting_advanced.wren vendored Normal file
View File

@ -0,0 +1,24 @@
// retoor <retoor@molodetz.nl>
import "markdown" for Markdown
System.print(Markdown.toHtml("__bold underscore__")) // expect: <p><strong>bold underscore</strong></p>
var mixed = Markdown.toHtml("This is **bold** and *italic* text")
System.print(mixed.contains("<strong>bold</strong>")) // expect: true
System.print(mixed.contains("<em>italic</em>")) // expect: true
var code = Markdown.toHtml("Use `print()` function")
System.print(code.contains("<code>print()</code>")) // expect: true
var multi = Markdown.toHtml("**bold** and `code` and *italic*")
System.print(multi.contains("<strong>bold</strong>")) // expect: true
System.print(multi.contains("<code>code</code>")) // expect: true
System.print(multi.contains("<em>italic</em>")) // expect: true
var unclosed = Markdown.toHtml("This has *unclosed italic")
System.print(unclosed.contains("<p>")) // expect: true
var empty = Markdown.toHtml("**bold****more**")
System.print(empty.contains("<strong>bold</strong>")) // expect: true
System.print(empty.contains("<strong>more</strong>")) // expect: true

49
test/markdown/from_html.wren vendored Normal file
View File

@ -0,0 +1,49 @@
// retoor <retoor@molodetz.nl>
import "markdown" for Markdown
System.print(Markdown.fromHtml("<h1>Title</h1>")) // expect: # Title
System.print(Markdown.fromHtml("<h2>Subtitle</h2>")) // expect: ## Subtitle
System.print(Markdown.fromHtml("<h3>Section</h3>")) // expect: ### Section
System.print(Markdown.fromHtml("<h4>Subsection</h4>")) // expect: #### Subsection
System.print(Markdown.fromHtml("<h5>Minor</h5>")) // expect: ##### Minor
System.print(Markdown.fromHtml("<h6>Smallest</h6>")) // expect: ###### Smallest
System.print(Markdown.fromHtml("<strong>bold</strong>")) // expect: **bold**
System.print(Markdown.fromHtml("<b>bold</b>")) // expect: **bold**
System.print(Markdown.fromHtml("<em>italic</em>")) // expect: *italic*
System.print(Markdown.fromHtml("<i>italic</i>")) // expect: *italic*
System.print(Markdown.fromHtml("<code>code</code>")) // expect: `code`
System.print(Markdown.fromHtml("<del>deleted</del>")) // expect: ~~deleted~~
System.print(Markdown.fromHtml("<s>strike</s>")) // expect: ~~strike~~
System.print(Markdown.fromHtml("<a href=\"https://example.com\">Example</a>")) // expect: [Example](https://example.com)
System.print(Markdown.fromHtml("<img src=\"image.png\" alt=\"My Image\">")) // expect: ![My Image](image.png)
var ul = Markdown.fromHtml("<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>")
System.print(ul.contains("- Item 1")) // expect: true
System.print(ul.contains("- Item 2")) // expect: true
System.print(ul.contains("- Item 3")) // expect: true
var ol = Markdown.fromHtml("<ol><li>First</li><li>Second</li><li>Third</li></ol>")
System.print(ol.contains("1. First")) // expect: true
System.print(ol.contains("2. Second")) // expect: true
System.print(ol.contains("3. Third")) // expect: true
var bq = Markdown.fromHtml("<blockquote>Quote text</blockquote>")
System.print(bq.contains("> Quote text")) // expect: true
System.print(Markdown.fromHtml("<hr>").contains("---")) // expect: true
System.print(Markdown.fromHtml("<hr/>").contains("---")) // expect: true
var p = Markdown.fromHtml("<p>First paragraph</p><p>Second paragraph</p>")
System.print(p.contains("First paragraph")) // expect: true
System.print(p.contains("Second paragraph")) // expect: true
var br = Markdown.fromHtml("Line one<br>Line two")
System.print(br.contains("Line one")) // expect: true
System.print(br.contains("Line two")) // expect: true

78
test/markdown/from_html_advanced.wren vendored Normal file
View File

@ -0,0 +1,78 @@
// retoor <retoor@molodetz.nl>
import "markdown" for Markdown
var nested = Markdown.fromHtml("<p><strong>Bold <em>and italic</em> text</strong></p>")
System.print(nested.contains("**Bold *and italic* text**")) // expect: true
var mixed = Markdown.fromHtml("<p>Normal <strong>bold</strong> and <em>italic</em> and <code>code</code></p>")
System.print(mixed.contains("Normal **bold** and *italic* and `code`")) // expect: true
var codeBlock = Markdown.fromHtml("<pre><code>function test() {\n return true;\n}</code></pre>")
System.print(codeBlock.contains("```")) // expect: true
System.print(codeBlock.contains("function test()")) // expect: true
var script = Markdown.fromHtml("<p>Hello</p><script>alert('xss');</script><p>World</p>")
System.print(script.contains("Hello")) // expect: true
System.print(script.contains("World")) // expect: true
System.print(script.contains("alert")) // expect: false
System.print(script.contains("script")) // expect: false
var style = Markdown.fromHtml("<p>Content</p><style>.red { color: red; }</style>")
System.print(style.contains("Content")) // expect: true
System.print(style.contains("color")) // expect: false
var entities = Markdown.fromHtml("<p>5 &gt; 3 &amp;&amp; 3 &lt; 5</p>")
System.print(entities.contains(">")) // expect: true
System.print(entities.contains("<")) // expect: true
System.print(entities.contains("&")) // expect: true
var doc = """
<html>
<head><title>Test</title></head>
<body>
<h1>Main Title</h1>
<p>Introduction paragraph with <strong>bold</strong> text.</p>
<h2>Section One</h2>
<ul>
<li>Item A</li>
<li>Item B</li>
</ul>
<blockquote>A wise quote</blockquote>
<hr>
<p>Footer content</p>
</body>
</html>
"""
var result = Markdown.fromHtml(doc)
System.print(result.contains("# Main Title")) // expect: true
System.print(result.contains("**bold**")) // expect: true
System.print(result.contains("## Section One")) // expect: true
System.print(result.contains("- Item A")) // expect: true
System.print(result.contains("> A wise quote")) // expect: true
System.print(result.contains("---")) // expect: true
var linkNested = Markdown.fromHtml("<a href=\"/page\"><strong>Bold Link</strong></a>")
System.print(linkNested.contains("[**Bold Link**](/page)")) // expect: true
var container = Markdown.fromHtml("<div><section><article>Content</article></section></div>")
System.print(container.contains("Content")) // expect: true
System.print(container.contains("<div>")) // expect: false
System.print(container.contains("<section>")) // expect: false
System.print(Markdown.fromHtml("<img src=\"a.png\" alt=\"test\"/>").contains("![test](a.png)")) // expect: true
System.print(Markdown.fromHtml("<br/>").trim() == "") // expect: true
System.print(Markdown.fromHtml("<p></p>").trim() == "") // expect: true
System.print(Markdown.fromHtml("<strong></strong>").trim() == "****") // expect: true
var ws = Markdown.fromHtml("<p> spaced content </p>")
System.print(ws.contains("spaced content")) // expect: true
var multi = Markdown.fromHtml("<p>Para 1</p><p>Para 2</p><p>Para 3</p>")
var lines = multi.split("\n")
var nonEmpty = 0
for (line in lines) {
if (line.trim().count > 0) nonEmpty = nonEmpty + 1
}
System.print(nonEmpty >= 3) // expect: true

11
test/markdown/horizontal_rules.wren vendored Normal file
View File

@ -0,0 +1,11 @@
// retoor <retoor@molodetz.nl>
import "markdown" for Markdown
System.print(Markdown.toHtml("---").contains("<hr>")) // expect: true
System.print(Markdown.toHtml("***").contains("<hr>")) // expect: true
System.print(Markdown.toHtml("___").contains("<hr>")) // expect: true
System.print(Markdown.toHtml("-----").contains("<hr>")) // expect: true
System.print(Markdown.toHtml("*****").contains("<hr>")) // expect: true
System.print(Markdown.toHtml("_____").contains("<hr>")) // expect: true

21
test/markdown/links_advanced.wren vendored Normal file
View File

@ -0,0 +1,21 @@
// retoor <retoor@molodetz.nl>
import "markdown" for Markdown
var multi = Markdown.toHtml("[First](http://a.com) and [Second](http://b.com)")
System.print(multi.contains("<a href=\"http://a.com\">First</a>")) // expect: true
System.print(multi.contains("<a href=\"http://b.com\">Second</a>")) // expect: true
var withPath = Markdown.toHtml("[Docs](/docs/api/index.html)")
System.print(withPath.contains("<a href=\"/docs/api/index.html\">Docs</a>")) // expect: true
var imgAlt = Markdown.toHtml("![A cat](cat.jpg)")
System.print(imgAlt.contains("<img src=\"cat.jpg\" alt=\"A cat\">")) // expect: true
var imgUrl = Markdown.toHtml("![Logo](https://example.com/logo.png)")
System.print(imgUrl.contains("<img src=\"https://example.com/logo.png\" alt=\"Logo\">")) // expect: true
var textAround = Markdown.toHtml("Click [here](url) now")
System.print(textAround.contains("Click ")) // expect: true
System.print(textAround.contains("<a href=\"url\">here</a>")) // expect: true
System.print(textAround.contains(" now")) // expect: true

22
test/markdown/lists_advanced.wren vendored Normal file
View File

@ -0,0 +1,22 @@
// retoor <retoor@molodetz.nl>
import "markdown" for Markdown
var star = Markdown.toHtml("* item a\n* item b")
System.print(star.contains("<ul>")) // expect: true
System.print(star.contains("<li>item a</li>")) // expect: true
System.print(star.contains("<li>item b</li>")) // expect: true
var plus = Markdown.toHtml("+ item x\n+ item y")
System.print(plus.contains("<ul>")) // expect: true
System.print(plus.contains("<li>item x</li>")) // expect: true
System.print(plus.contains("<li>item y</li>")) // expect: true
var multiDigit = Markdown.toHtml("10. tenth\n11. eleventh")
System.print(multiDigit.contains("<ol>")) // expect: true
System.print(multiDigit.contains("<li>tenth</li>")) // expect: true
System.print(multiDigit.contains("<li>eleventh</li>")) // expect: true
var formatted = Markdown.toHtml("- **bold item**\n- *italic item*")
System.print(formatted.contains("<li><strong>bold item</strong></li>")) // expect: true
System.print(formatted.contains("<li><em>italic item</em></li>")) // expect: true

40
test/markdown/mixed_content.wren vendored Normal file
View File

@ -0,0 +1,40 @@
// retoor <retoor@molodetz.nl>
import "markdown" for Markdown
var doc = "# Title
This is a paragraph with **bold** text.
- Item 1
- Item 2
Another paragraph.
> A quote
---
## Subtitle
1. First
2. Second"
var html = Markdown.toHtml(doc)
System.print(html.contains("<h1>Title</h1>")) // expect: true
System.print(html.contains("<strong>bold</strong>")) // expect: true
System.print(html.contains("<ul>")) // expect: true
System.print(html.contains("<li>Item 1</li>")) // expect: true
System.print(html.contains("<blockquote>")) // expect: true
System.print(html.contains("<hr>")) // expect: true
System.print(html.contains("<h2>Subtitle</h2>")) // expect: true
System.print(html.contains("<ol>")) // expect: true
System.print(html.contains("<li>First</li>")) // expect: true
var linkInList = Markdown.toHtml("- [Link](http://example.com)")
System.print(linkInList.contains("<li>")) // expect: true
System.print(linkInList.contains("<a href=\"http://example.com\">Link</a>")) // expect: true
var codeInHeading = Markdown.toHtml("# Using `print()`")
System.print(codeInHeading.contains("<h1>")) // expect: true
System.print(codeInHeading.contains("<code>print()</code>")) // expect: true

17
test/markdown/paragraphs.wren vendored Normal file
View File

@ -0,0 +1,17 @@
// retoor <retoor@molodetz.nl>
import "markdown" for Markdown
var p = Markdown.toHtml("This is a paragraph.")
System.print(p) // expect: <p>This is a paragraph.</p>
var multi = Markdown.toHtml("First paragraph.\n\nSecond paragraph.")
System.print(multi.contains("<p>First paragraph.</p>")) // expect: true
System.print(multi.contains("<p>Second paragraph.</p>")) // expect: true
var afterHeading = Markdown.toHtml("# Title\n\nSome text here.")
System.print(afterHeading.contains("<h1>Title</h1>")) // expect: true
System.print(afterHeading.contains("<p>Some text here.</p>")) // expect: true
var empty = Markdown.toHtml("")
System.print(empty) // expect:

21
test/markdown/safe_mode.wren vendored Normal file
View File

@ -0,0 +1,21 @@
// retoor <retoor@molodetz.nl>
import "markdown" for Markdown
var unsafe = Markdown.toHtml("<script>alert('xss')</script>")
System.print(unsafe.contains("<script>")) // expect: true
var safe = Markdown.toHtml("<script>alert('xss')</script>", {"safeMode": true})
System.print(safe.contains("<script>")) // expect: false
System.print(safe.contains("&lt;script&gt;")) // expect: true
var safeCode = Markdown.toHtml("```\n<div>test</div>\n```", {"safeMode": true})
System.print(safeCode.contains("<div>")) // expect: false
System.print(safeCode.contains("&lt;div&gt;")) // expect: true
var safeInline = Markdown.toHtml("Text with <b>html</b> inside", {"safeMode": true})
System.print(safeInline.contains("<b>")) // expect: false
System.print(safeInline.contains("&lt;b&gt;")) // expect: true
var ampersand = Markdown.toHtml("A & B", {"safeMode": true})
System.print(ampersand.contains("&amp;")) // expect: true

13
test/pathlib/absolute.wren vendored Normal file
View File

@ -0,0 +1,13 @@
// retoor <retoor@molodetz.nl>
import "pathlib" for Path
var p1 = Path.new("/home/user")
System.print(p1.isAbsolute) // expect: true
System.print(p1.root) // expect: /
System.print(p1.anchor) // expect: /
var p2 = Path.new("relative/path")
System.print(p2.isAbsolute) // expect: false
System.print(p2.root) // expect:
System.print(p2.anchor) // expect:

15
test/pathlib/construction.wren vendored Normal file
View File

@ -0,0 +1,15 @@
// retoor <retoor@molodetz.nl>
import "pathlib" for Path
var p1 = Path.new("/home/user/file.txt")
System.print(p1.toString) // expect: /home/user/file.txt
var p2 = Path.new("")
System.print(p2.toString) // expect:
var p3 = Path.new(p1)
System.print(p3.toString) // expect: /home/user/file.txt
var p4 = Path.new("/home") / "user" / "file.txt"
System.print(p4.toString) // expect: /home/user/file.txt

27
test/pathlib/name_stem_suffix.wren vendored Normal file
View File

@ -0,0 +1,27 @@
// retoor <retoor@molodetz.nl>
import "pathlib" for Path
var p1 = Path.new("/home/user/file.tar.gz")
System.print(p1.name) // expect: file.tar.gz
System.print(p1.stem) // expect: file.tar
System.print(p1.suffix) // expect: .gz
System.print(p1.suffixes) // expect: [.tar, .gz]
var p2 = Path.new("/home/user/file.txt")
System.print(p2.name) // expect: file.txt
System.print(p2.stem) // expect: file
System.print(p2.suffix) // expect: .txt
System.print(p2.suffixes) // expect: [.txt]
var p3 = Path.new("/home/user/noext")
System.print(p3.name) // expect: noext
System.print(p3.stem) // expect: noext
System.print(p3.suffix) // expect:
System.print(p3.suffixes) // expect: []
var p4 = Path.new("/home/user/.hidden")
System.print(p4.name) // expect: .hidden
System.print(p4.stem) // expect: .hidden
System.print(p4.suffix) // expect:
System.print(p4.suffixes) // expect: []

21
test/pathlib/navigation.wren vendored Normal file
View File

@ -0,0 +1,21 @@
// retoor <retoor@molodetz.nl>
import "pathlib" for Path
var p1 = Path.new("/home/user/file.txt")
System.print(p1.parent) // expect: /home/user
System.print(p1.parent.parent) // expect: /home
System.print(p1.parent.parent.parent) // expect: /
var p2 = Path.new("/home/user/file.txt")
var parents = p2.parents
System.print(parents.count) // expect: 3
System.print(parents[0]) // expect: /home/user
System.print(parents[1]) // expect: /home
System.print(parents[2]) // expect: /
var p3 = Path.new("relative/path")
System.print(p3.parent) // expect: relative
var p4 = Path.new("file.txt")
System.print(p4.parent) // expect: .

18
test/pathlib/parts.wren vendored Normal file
View File

@ -0,0 +1,18 @@
// retoor <retoor@molodetz.nl>
import "pathlib" for Path
var p1 = Path.new("/home/user/file.txt")
System.print(p1.parts) // expect: [/, home, user, file.txt]
var p2 = Path.new("relative/path/file.txt")
System.print(p2.parts) // expect: [relative, path, file.txt]
var p3 = Path.new("")
System.print(p3.parts) // expect: []
var p4 = Path.new("/")
System.print(p4.parts) // expect: [/]
var p5 = Path.new("file.txt")
System.print(p5.parts) // expect: [file.txt]

44
test/strutil/basic.wren vendored Normal file
View File

@ -0,0 +1,44 @@
// retoor <retoor@molodetz.nl>
import "strutil" for Str
System.print(Str.toLower("HELLO World")) // expect: hello world
System.print(Str.toLower("abc123")) // expect: abc123
System.print(Str.toLower("")) // expect:
System.print(Str.toUpper("hello World")) // expect: HELLO WORLD
System.print(Str.toUpper("ABC123")) // expect: ABC123
System.print(Str.toUpper("")) // expect:
System.print(Str.hexEncode("ABC")) // expect: 414243
System.print(Str.hexEncode("\x00\xFF")) // expect: 00ff
System.print(Str.hexEncode("")) // expect:
System.print(Str.hexDecode("414243")) // expect: ABC
System.print(Str.hexDecode("")) // expect:
System.print(Str.repeat("ab", 3)) // expect: ababab
System.print(Str.repeat("x", 5)) // expect: xxxxx
System.print(Str.repeat("", 10)) // expect:
System.print(Str.padLeft("hi", 5, " ")) // expect: hi
System.print(Str.padLeft("hello", 3, " ")) // expect: hello
System.print(Str.padLeft("x", 4, "0")) // expect: 000x
System.print("[" + Str.padRight("hi", 5, " ") + "]") // expect: [hi ]
System.print(Str.padRight("hello", 3, " ")) // expect: hello
System.print(Str.padRight("x", 4, "0")) // expect: x000
System.print(Str.escapeHtml("<div class=\"test\">A & B</div>")) // expect: &lt;div class=&quot;test&quot;&gt;A &amp; B&lt;/div&gt;
System.print(Str.escapeHtml("hello")) // expect: hello
System.print(Str.escapeHtml("it's")) // expect: it&#39;s
System.print(Str.escapeJson("hello")) // expect: "hello"
System.print(Str.escapeJson("a\"b")) // expect: "a\"b"
System.print(Str.escapeJson("line1\nline2")) // expect: "line1\nline2"
System.print(Str.urlEncode("hello world")) // expect: hello+world
System.print(Str.urlEncode("test")) // expect: test
System.print(Str.urlDecode("hello+world")) // expect: hello world
System.print(Str.urlDecode("test")) // expect: test

27
test/web/client_url_parse.wren vendored Normal file
View File

@ -0,0 +1,27 @@
// retoor <retoor@molodetz.nl>
import "web" for Client
var parsed1 = Client.parseUrl_("http://example.com/path")
System.print(parsed1["scheme"]) // expect: http
System.print(parsed1["host"]) // expect: example.com
System.print(parsed1["port"]) // expect: 80
System.print(parsed1["path"]) // expect: /path
var parsed2 = Client.parseUrl_("https://example.com/path")
System.print(parsed2["scheme"]) // expect: https
System.print(parsed2["port"]) // expect: 443
var parsed3 = Client.parseUrl_("http://example.com:8080/api/v1")
System.print(parsed3["host"]) // expect: example.com
System.print(parsed3["port"]) // expect: 8080
System.print(parsed3["path"]) // expect: /api/v1
var parsed4 = Client.parseUrl_("https://api.example.com")
System.print(parsed4["host"]) // expect: api.example.com
System.print(parsed4["path"]) // expect: /
var parsed5 = Client.parseUrl_("http://localhost:3000/")
System.print(parsed5["host"]) // expect: localhost
System.print(parsed5["port"]) // expect: 3000
System.print(parsed5["path"]) // expect: /

36
test/web/request.wren vendored Normal file
View File

@ -0,0 +1,36 @@
// retoor <retoor@molodetz.nl>
import "web" for Request
var headers = {
"Content-Type": "application/json",
"Accept": "text/html",
"Cookie": "session=abc123; user=alice"
}
var req = Request.new_("POST", "/api/users", {"page": "1", "limit": "10"}, headers, "{\"name\":\"Alice\"}", {"id": "42"}, null)
System.print(req.method) // expect: POST
System.print(req.path) // expect: /api/users
System.print(req.query["page"]) // expect: 1
System.print(req.query["limit"]) // expect: 10
System.print(req.params["id"]) // expect: 42
System.print(req.body) // expect: {"name":"Alice"}
System.print(req.header("Content-Type")) // expect: application/json
System.print(req.header("content-type")) // expect: application/json
System.print(req.header("ACCEPT")) // expect: text/html
System.print(req.header("X-Missing") == null) // expect: true
var json = req.json
System.print(json["name"]) // expect: Alice
var cookies = req.cookies
System.print(cookies["session"]) // expect: abc123
System.print(cookies["user"]) // expect: alice
var formBody = "username=bob&password=secret"
var formReq = Request.new_("POST", "/login", {}, {"Content-Type": "application/x-www-form-urlencoded"}, formBody, {}, null)
var form = formReq.form
System.print(form["username"]) // expect: bob
System.print(form["password"]) // expect: secret

39
test/web/response_advanced.wren vendored Normal file
View File

@ -0,0 +1,39 @@
// retoor <retoor@molodetz.nl>
import "web" for Response
var r1 = Response.redirect("/dashboard", 301)
System.print(r1.status) // expect: 301
System.print(r1.headers["Location"]) // expect: /dashboard
var r2 = Response.new()
r2.status = 404
r2.body = "Not Found"
System.print(r2.status) // expect: 404
System.print(r2.body) // expect: Not Found
var r3 = Response.new()
r3.header("X-Custom", "value1")
r3.header("X-Another", "value2")
System.print(r3.headers["X-Custom"]) // expect: value1
System.print(r3.headers["X-Another"]) // expect: value2
var r4 = Response.new()
r4.cookie("session", "abc123")
r4.body = "test"
var built = r4.build()
System.print(built.contains("Set-Cookie: session=abc123")) // expect: true
var r5 = Response.new()
r5.cookie("auth", "token", {"path": "/", "httpOnly": true, "maxAge": 3600})
r5.body = "test"
var built2 = r5.build()
System.print(built2.contains("Path=/")) // expect: true
System.print(built2.contains("HttpOnly")) // expect: true
System.print(built2.contains("Max-Age=3600")) // expect: true
var r6 = Response.text("Hello World")
var httpResponse = r6.build()
System.print(httpResponse.contains("HTTP/1.1 200 OK")) // expect: true
System.print(httpResponse.contains("Content-Length:")) // expect: true
System.print(httpResponse.contains("Hello World")) // expect: true

42
test/web/router_advanced.wren vendored Normal file
View File

@ -0,0 +1,42 @@
// retoor <retoor@molodetz.nl>
import "web" for Router
var router = Router.new()
router.get("/api/v1/*", Fn.new { |r| "wildcard" })
router.put("/users/:id", Fn.new { |r| "put user" })
router.delete("/users/:id", Fn.new { |r| "delete user" })
router.patch("/users/:id", Fn.new { |r| "patch user" })
router.post("/users", Fn.new { |r| "create user" })
var m1 = router.match("GET", "/api/v1/anything")
System.print(m1 != null) // expect: true
var m2 = router.match("GET", "/api/v1/nested/path")
System.print(m2 != null) // expect: true
var m3 = router.match("PUT", "/users/456")
System.print(m3 != null) // expect: true
System.print(m3["params"]["id"]) // expect: 456
var m4 = router.match("DELETE", "/users/789")
System.print(m4 != null) // expect: true
System.print(m4["params"]["id"]) // expect: 789
var m5 = router.match("PATCH", "/users/101")
System.print(m5 != null) // expect: true
var m6 = router.match("POST", "/users")
System.print(m6 != null) // expect: true
var m7 = router.match("OPTIONS", "/users")
System.print(m7 == null) // expect: true
var router2 = Router.new()
router2.get("/posts/:postId/comments/:commentId", Fn.new { |r| "comment" })
var m8 = router2.match("GET", "/posts/10/comments/20")
System.print(m8 != null) // expect: true
System.print(m8["params"]["postId"]) // expect: 10
System.print(m8["params"]["commentId"]) // expect: 20

34
test/web/session_advanced.wren vendored Normal file
View File

@ -0,0 +1,34 @@
// retoor <retoor@molodetz.nl>
import "web" for Session, SessionStore
var store = SessionStore.new()
var session = store.create()
session["name"] = "Alice"
session["age"] = 30
session["active"] = true
System.print(session["name"]) // expect: Alice
System.print(session["age"]) // expect: 30
System.print(session["active"]) // expect: true
System.print(session["nonexistent"] == null) // expect: true
System.print(session.isModified) // expect: true
session.remove("age")
System.print(session["age"] == null) // expect: true
store.save(session)
var data = session.data
System.print(data.containsKey("name")) // expect: true
System.print(data.containsKey("age")) // expect: false
System.print(data["active"]) // expect: true
var session2 = store.create()
var session3 = store.create()
System.print(session.id != session2.id) // expect: true
System.print(session2.id != session3.id) // expect: true
System.print(session.id.count) // expect: 36
System.print(session2.id.count) // expect: 36

58
test/web/view.wren vendored Normal file
View File

@ -0,0 +1,58 @@
// retoor <retoor@molodetz.nl>
import "web" for View, Response, Request
class TestView is View {
construct new() {}
get(request) {
return Response.json({"method": "GET"})
}
post(request) {
return Response.json({"method": "POST"})
}
put(request) {
return Response.json({"method": "PUT"})
}
delete(request) {
return Response.json({"method": "DELETE"})
}
}
class GetOnlyView is View {
construct new() {}
get(request) {
return Response.text("GET only")
}
}
var mockGet = Request.new_("GET", "/test", {}, {}, "", {}, null)
var mockPost = Request.new_("POST", "/test", {}, {}, "", {}, null)
var mockPut = Request.new_("PUT", "/test", {}, {}, "", {}, null)
var mockDelete = Request.new_("DELETE", "/test", {}, {}, "", {}, null)
var mockPatch = Request.new_("PATCH", "/test", {}, {}, "", {}, null)
var view = TestView.new()
var r1 = view.dispatch(mockGet)
System.print(r1.body.contains("GET")) // expect: true
var r2 = view.dispatch(mockPost)
System.print(r2.body.contains("POST")) // expect: true
var r3 = view.dispatch(mockPut)
System.print(r3.body.contains("PUT")) // expect: true
var r4 = view.dispatch(mockDelete)
System.print(r4.body.contains("DELETE")) // expect: true
var getOnlyView = GetOnlyView.new()
var r5 = getOnlyView.dispatch(mockGet)
System.print(r5.body) // expect: GET only
var r6 = getOnlyView.dispatch(mockPost)
System.print(r6.status) // expect: 405