This commit is contained in:
retoor 2026-01-26 10:21:03 +01:00
parent fe2f087d9f
commit fbc1a6e294
40 changed files with 3478 additions and 547 deletions

View File

@ -124,6 +124,8 @@ sections:
title: "WebSocket Chat"
- file: "database-app"
title: "Database App"
- file: "dataset-orm"
title: "Dataset ORM"
- file: "template-rendering"
title: "Templates"
- file: "cli-tool"

View File

@ -9,7 +9,7 @@
{% block 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>
<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, images, and GitHub Flavored Markdown (GFM) extensions like tables, task lists, and autolinks.</p>
<pre><code>import "markdown" for Markdown</code></pre>
@ -121,8 +121,12 @@ System.print(md) // # Hello World</code></pre>
```
Code block
Multiple lines
```
```javascript
const x = 1;
```</code></pre>
<p>Inline code uses <code>&lt;code&gt;</code>, blocks use <code>&lt;pre&gt;&lt;code&gt;</code>.</p>
<p>Inline code uses <code>&lt;code&gt;</code>, blocks use <code>&lt;pre&gt;&lt;code&gt;</code>. Language identifiers after the opening fence add a <code>class="language-{lang}"</code> attribute to the code element for syntax highlighting integration.</p>
<h3>Lists</h3>
<pre><code>Unordered:
@ -137,6 +141,24 @@ Ordered:
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>Task Lists (GFM)</h3>
<pre><code>- [ ] Unchecked task
- [x] Checked task
- [X] Also checked</code></pre>
<p>Creates <code>&lt;ul class="task-list"&gt;</code> with checkbox inputs. Checkboxes are rendered as disabled to indicate they are display-only.</p>
<h3>Tables (GFM)</h3>
<pre><code>| Header 1 | Header 2 | Header 3 |
|----------|:--------:|---------:|
| Left | Center | Right |
| Cell | Cell | Cell |</code></pre>
<p>Creates HTML tables with <code>&lt;thead&gt;</code> and <code>&lt;tbody&gt;</code>. Column alignment is specified in the separator row using colons: <code>:---</code> for left, <code>:---:</code> for center, <code>---:</code> for right.</p>
<h3>Autolinks (GFM)</h3>
<pre><code>Visit https://example.com for more info.
Email me at user@example.com</code></pre>
<p>Bare URLs starting with <code>http://</code> or <code>https://</code> and email addresses are automatically converted to clickable links.</p>
<h3>Links and Images</h3>
<pre><code>[Link text](https://example.com)
![Alt text](image.png)</code></pre>
@ -213,6 +235,10 @@ ___</code></pre>
<td><code>&lt;del&gt;</code>, <code>&lt;s&gt;</code></td>
<td><code>~~text~~</code></td>
</tr>
<tr>
<td><code>&lt;table&gt;</code></td>
<td>GFM table syntax</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>
@ -263,7 +289,7 @@ var page = "&lt;html&gt;
File.write("post.html", page)</code></pre>
<h3>Code Blocks with Language Hints</h3>
<h3>Code Blocks with Language Classes</h3>
<pre><code>import "markdown" for Markdown
var md = "
@ -274,7 +300,35 @@ 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>
// &lt;pre&gt;&lt;code class="language-wren"&gt;System.print("Hello, World!")&lt;/code&gt;&lt;/pre&gt;</code></pre>
<h3>Tables</h3>
<pre><code>import "markdown" for Markdown
var md = "
| Name | Age | City |
|-------|----:|:----------|
| Alice | 30 | New York |
| Bob | 25 | London |
"
var html = Markdown.toHtml(md)
System.print(html)</code></pre>
<h3>Task Lists</h3>
<pre><code>import "markdown" for Markdown
var md = "
## Project Tasks
- [x] Design database schema
- [x] Implement API endpoints
- [ ] Write unit tests
- [ ] Deploy to production
"
var html = Markdown.toHtml(md)
System.print(html)</code></pre>
<h3>Combining with Templates</h3>
<pre><code>import "markdown" for Markdown
@ -329,6 +383,6 @@ System.print(clean) // Safe **content**</code></pre>
<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>
<p>Language identifiers after code fences (e.g., <code>```wren</code>) are added as <code>class="language-{lang}"</code> to the code element. This is compatible with syntax highlighters like Prism.js or highlight.js.</p>
</div>
{% endblock %}

View File

@ -4,7 +4,7 @@
{% set page_title = "Database Application" %}
{% set breadcrumb = [{"url": "tutorials/index.html", "title": "Tutorials"}, {"title": "Database App"}] %}
{% set prev_page = {"url": "tutorials/websocket-chat.html", "title": "WebSocket Chat"} %}
{% set next_page = {"url": "tutorials/template-rendering.html", "title": "Template Rendering"} %}
{% set next_page = {"url": "tutorials/dataset-orm.html", "title": "Dataset ORM"} %}
{% block article %}
<h1>Database Application</h1>

View File

@ -0,0 +1,628 @@
{# retoor <retoor@molodetz.nl> #}
{% extends 'page.html' %}
{% set page_title = "Dataset ORM Tutorial" %}
{% set breadcrumb = [{"url": "tutorials/index.html", "title": "Tutorials"}, {"title": "Dataset ORM"}] %}
{% set prev_page = {"url": "tutorials/database-app.html", "title": "Database App"} %}
{% set next_page = {"url": "tutorials/template-rendering.html", "title": "Template Rendering"} %}
{% block article %}
<h1>Dataset ORM Tutorial</h1>
<p>The <code>dataset</code> module provides an ORM-like interface for SQLite databases with automatic schema management. This tutorial covers everything from basic CRUD operations to advanced querying patterns.</p>
<h2>Table of Contents</h2>
<ul>
<li><a href="#introduction">Introduction</a></li>
<li><a href="#getting-started">Getting Started</a></li>
<li><a href="#insert-operations">Insert Operations</a></li>
<li><a href="#querying-data">Querying Data</a></li>
<li><a href="#query-operators">Query Operators</a></li>
<li><a href="#update-operations">Update Operations</a></li>
<li><a href="#delete-operations">Delete Operations</a></li>
<li><a href="#schema-evolution">Schema Evolution</a></li>
<li><a href="#json-fields">Working with JSON Fields</a></li>
<li><a href="#multiple-tables">Multiple Tables</a></li>
<li><a href="#building-an-application">Building a Complete Application</a></li>
<li><a href="#best-practices">Best Practices</a></li>
</ul>
<h2 id="introduction">Introduction</h2>
<p>Traditional database programming requires you to define schemas upfront, write SQL queries manually, and handle type conversions. The dataset module eliminates this boilerplate:</p>
<ul>
<li><strong>Auto-schema</strong> - Tables and columns are created automatically when you insert data</li>
<li><strong>No SQL</strong> - Query using maps with intuitive operators</li>
<li><strong>Type conversion</strong> - Maps and Lists are automatically serialized to JSON</li>
<li><strong>Soft delete</strong> - Records are never permanently lost by default</li>
<li><strong>UUID primary keys</strong> - Every record gets a unique identifier</li>
</ul>
<h2 id="getting-started">Getting Started</h2>
<h3>Opening a Database</h3>
<p>Create a file-based database for persistent storage:</p>
<pre><code>import "dataset" for Dataset
var db = Dataset.open("myapp.db")
var users = db["users"]
db.close()</code></pre>
<p>Or use an in-memory database for testing:</p>
<pre><code>import "dataset" for Dataset
var db = Dataset.memory()
var users = db["users"]
db.close()</code></pre>
<h3>Basic Structure</h3>
<p>Every table automatically has three fields:</p>
<table>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
<tr>
<td><code>uid</code></td>
<td>TEXT (UUID)</td>
<td>Primary key, auto-generated</td>
</tr>
<tr>
<td><code>created_at</code></td>
<td>TEXT (ISO datetime)</td>
<td>Timestamp when record was created</td>
</tr>
<tr>
<td><code>deleted_at</code></td>
<td>TEXT (ISO datetime)</td>
<td>Timestamp when soft-deleted (null if active)</td>
</tr>
</table>
<h2 id="insert-operations">Insert Operations</h2>
<h3>Basic Insert</h3>
<pre><code>import "dataset" for Dataset
var db = Dataset.memory()
var users = db["users"]
var user = users.insert({
"name": "Alice",
"email": "alice@example.com",
"age": 28
})
System.print(user["uid"]) // e.g., "550e8400-e29b-41d4-a716-446655440000"
System.print(user["created_at"]) // e.g., "2024-01-15T10:30:00"
System.print(user["name"]) // "Alice"
db.close()</code></pre>
<h3>Custom UID</h3>
<p>Provide your own UID if needed:</p>
<pre><code>var user = users.insert({
"uid": "custom-id-12345",
"name": "Bob"
})</code></pre>
<h3>Type Inference</h3>
<p>Columns are created with appropriate SQLite types:</p>
<pre><code>users.insert({
"name": "Charlie", // TEXT
"age": 30, // INTEGER
"score": 95.5, // REAL
"active": true, // INTEGER (1 or 0)
"tags": ["a", "b"], // TEXT (JSON array)
"settings": { // TEXT (JSON object)
"theme": "dark"
}
})</code></pre>
<h2 id="querying-data">Querying Data</h2>
<h3>Find All Records</h3>
<pre><code>var allUsers = users.all()
for (user in allUsers) {
System.print("%(user["name"]) - %(user["email"])")
}</code></pre>
<h3>Find by Conditions</h3>
<pre><code>var admins = users.find({"role": "admin"})
var youngAdmins = users.find({
"role": "admin",
"age": 25
})</code></pre>
<h3>Find One Record</h3>
<pre><code>var user = users.findOne({"email": "alice@example.com"})
if (user != null) {
System.print("Found: %(user["name"])")
} else {
System.print("User not found")
}</code></pre>
<h3>Count Records</h3>
<pre><code>var totalUsers = users.count()
System.print("Total users: %(totalUsers)")</code></pre>
<h3>Graceful Handling of Missing Columns</h3>
<p>Querying a column that does not exist returns an empty list instead of crashing:</p>
<pre><code>var result = users.find({"nonexistent_column": "value"})
System.print(result.count) // 0</code></pre>
<h2 id="query-operators">Query Operators</h2>
<p>Use double-underscore suffixes for comparison operators:</p>
<h3>Comparison Operators</h3>
<pre><code>var adults = users.find({"age__gte": 18})
var teenagers = users.find({
"age__gte": 13,
"age__lt": 20
})
var notAdmin = users.find({"role__ne": "admin"})</code></pre>
<h3>Complete Operator Reference</h3>
<table>
<tr>
<th>Suffix</th>
<th>SQL</th>
<th>Example</th>
</tr>
<tr>
<td><code>__gt</code></td>
<td><code>&gt;</code></td>
<td><code>{"age__gt": 18}</code> - age greater than 18</td>
</tr>
<tr>
<td><code>__lt</code></td>
<td><code>&lt;</code></td>
<td><code>{"price__lt": 100}</code> - price less than 100</td>
</tr>
<tr>
<td><code>__gte</code></td>
<td><code>&gt;=</code></td>
<td><code>{"score__gte": 90}</code> - score 90 or higher</td>
</tr>
<tr>
<td><code>__lte</code></td>
<td><code>&lt;=</code></td>
<td><code>{"quantity__lte": 10}</code> - quantity 10 or less</td>
</tr>
<tr>
<td><code>__ne</code></td>
<td><code>!=</code></td>
<td><code>{"status__ne": "deleted"}</code> - status not deleted</td>
</tr>
<tr>
<td><code>__like</code></td>
<td><code>LIKE</code></td>
<td><code>{"name__like": "A\%"}</code> - name starts with A</td>
</tr>
<tr>
<td><code>__in</code></td>
<td><code>IN</code></td>
<td><code>{"status__in": ["active", "pending"]}</code></td>
</tr>
<tr>
<td><code>__null</code></td>
<td><code>IS NULL</code></td>
<td><code>{"deleted_at__null": true}</code> - is null</td>
</tr>
</table>
<h3>LIKE Patterns</h3>
<pre><code>var startsWithA = users.find({"name__like": "A\%"})
var containsSmith = users.find({"name__like": "\%Smith\%"})
var threeLetterNames = users.find({"name__like": "___"})</code></pre>
<div class="admonition note">
<div class="admonition-title">Note</div>
<p>In Wren strings, use <code>\%</code> for the SQL <code>%</code> wildcard and <code>_</code> for single character wildcard.</p>
</div>
<h3>IN Operator</h3>
<pre><code>var priorityUsers = users.find({
"role__in": ["admin", "moderator", "editor"]
})
var specificIds = users.find({
"uid__in": [
"550e8400-e29b-41d4-a716-446655440000",
"6ba7b810-9dad-11d1-80b4-00c04fd430c8"
]
})</code></pre>
<h3>NULL Checking</h3>
<pre><code>var usersWithEmail = users.find({"email__null": false})
var usersWithoutPhone = users.find({"phone__null": true})</code></pre>
<h3>Combining Conditions</h3>
<p>Multiple conditions are combined with AND:</p>
<pre><code>var results = products.find({
"category": "electronics",
"price__gte": 100,
"price__lte": 500,
"stock__gt": 0
})</code></pre>
<h2 id="update-operations">Update Operations</h2>
<h3>Basic Update</h3>
<pre><code>var user = users.findOne({"email": "alice@example.com"})
var changes = users.update({
"uid": user["uid"],
"name": "Alice Smith",
"age": 29
})
System.print("Rows affected: %(changes)")</code></pre>
<h3>Adding New Fields</h3>
<p>Updates can add new columns dynamically:</p>
<pre><code>users.update({
"uid": user["uid"],
"phone": "+1-555-0123",
"verified": true
})</code></pre>
<h2 id="delete-operations">Delete Operations</h2>
<h3>Soft Delete (Default)</h3>
<p>Soft delete sets <code>deleted_at</code> but keeps the record:</p>
<pre><code>var deleted = users.delete(user["uid"])
System.print(deleted) // true if found and deleted
var all = users.all()
// Soft-deleted records are excluded</code></pre>
<h3>Hard Delete</h3>
<p>Permanently remove a record:</p>
<pre><code>users.hardDelete(user["uid"])</code></pre>
<div class="admonition warning">
<div class="admonition-title">Warning</div>
<p>Hard delete is permanent. The record cannot be recovered.</p>
</div>
<h2 id="schema-evolution">Schema Evolution</h2>
<p>The dataset module handles schema changes automatically:</p>
<pre><code>import "dataset" for Dataset
var db = Dataset.memory()
var users = db["users"]
users.insert({"name": "Alice"})
System.print(users.columns) // {"uid": "TEXT", "created_at": "TEXT", "deleted_at": "TEXT", "name": "TEXT"}
users.insert({"name": "Bob", "email": "bob@example.com", "age": 30})
System.print(users.columns) // Now includes "email" and "age"
var alice = users.findOne({"name": "Alice"})
System.print(alice["email"]) // null (column exists but Alice has no value)
db.close()</code></pre>
<h2 id="json-fields">Working with JSON Fields</h2>
<h3>Storing Complex Data</h3>
<pre><code>import "dataset" for Dataset
var db = Dataset.memory()
var posts = db["posts"]
posts.insert({
"title": "Getting Started with Wren",
"tags": ["wren", "programming", "tutorial"],
"metadata": {
"author": "Alice",
"views": 1250,
"featured": true,
"related": [101, 102, 103]
}
})
var post = posts.findOne({"title": "Getting Started with Wren"})
System.print(post["tags"]) // ["wren", "programming", "tutorial"]
System.print(post["tags"][0]) // "wren"
System.print(post["metadata"]["author"]) // "Alice"
System.print(post["metadata"]["views"]) // 1250
System.print(post["metadata"]["related"][0]) // 101
db.close()</code></pre>
<h3>Nested Updates</h3>
<p>To update nested JSON, retrieve the full object, modify it, and save:</p>
<pre><code>var post = posts.findOne({"title": "Getting Started with Wren"})
var metadata = post["metadata"]
metadata["views"] = metadata["views"] + 1
posts.update({
"uid": post["uid"],
"metadata": metadata
})</code></pre>
<h2 id="multiple-tables">Multiple Tables</h2>
<pre><code>import "dataset" for Dataset
var db = Dataset.open("blog.db")
var authors = db["authors"]
var posts = db["posts"]
var comments = db["comments"]
var author = authors.insert({
"name": "Alice",
"bio": "Tech writer and developer"
})
var post = posts.insert({
"author_uid": author["uid"],
"title": "Introduction to Dataset",
"content": "The dataset module provides..."
})
comments.insert({
"post_uid": post["uid"],
"author_name": "Bob",
"content": "Great article!"
})
System.print(db.tables) // ["authors", "posts", "comments"]
var postComments = comments.find({"post_uid": post["uid"]})
System.print("Comments on post: %(postComments.count)")
db.close()</code></pre>
<h2 id="building-an-application">Building a Complete Application</h2>
<p>Let's build a task management application:</p>
<pre><code>import "dataset" for Dataset
import "io" for Stdin
class TaskManager {
construct new(dbPath) {
_db = Dataset.open(dbPath)
_tasks = _db["tasks"]
_categories = _db["categories"]
initDefaults()
}
initDefaults() {
if (_categories.count() == 0) {
_categories.insert({"name": "Work", "color": "blue"})
_categories.insert({"name": "Personal", "color": "green"})
_categories.insert({"name": "Shopping", "color": "orange"})
}
}
addTask(title, categoryName, priority) {
var category = _categories.findOne({"name": categoryName})
return _tasks.insert({
"title": title,
"category_uid": category != null ? category["uid"] : null,
"priority": priority,
"completed": false
})
}
listTasks() {
var tasks = _tasks.find({"completed": false})
for (task in tasks) {
var cat = task["category_uid"] != null ?
_categories.findOne({"uid": task["category_uid"]}) : null
var catName = cat != null ? cat["name"] : "None"
System.print("[%(task["priority"])] %(task["title"]) (%(catName))")
}
}
completeTask(title) {
var task = _tasks.findOne({"title": title})
if (task != null) {
_tasks.update({"uid": task["uid"], "completed": true})
return true
}
return false
}
listCategories() {
var categories = _categories.all()
for (cat in categories) {
var count = _tasks.find({
"category_uid": cat["uid"],
"completed": false
}).count
System.print("%(cat["name"]): %(count) tasks")
}
}
stats() {
var total = _tasks.count()
var completed = _tasks.find({"completed": true}).count
var pending = _tasks.find({"completed": false}).count
var highPriority = _tasks.find({
"completed": false,
"priority__gte": 3
}).count
System.print("Total: %(total)")
System.print("Completed: %(completed)")
System.print("Pending: %(pending)")
System.print("High priority: %(highPriority)")
}
close() {
_db.close()
}
}
var app = TaskManager.new("tasks.db")
app.addTask("Write documentation", "Work", 3)
app.addTask("Buy groceries", "Shopping", 2)
app.addTask("Learn Wren", "Personal", 3)
System.print("--- Tasks ---")
app.listTasks()
System.print("\n--- Categories ---")
app.listCategories()
System.print("\n--- Statistics ---")
app.stats()
app.completeTask("Write documentation")
System.print("\n--- After completion ---")
app.listTasks()
app.close()</code></pre>
<p>Output:</p>
<pre><code>--- Tasks ---
[3] Write documentation (Work)
[2] Buy groceries (Shopping)
[3] Learn Wren (Personal)
--- Categories ---
Work: 1 tasks
Personal: 1 tasks
Shopping: 1 tasks
--- Statistics ---
Total: 3
Completed: 0
Pending: 3
High priority: 2
--- After completion ---
[2] Buy groceries (Shopping)
[3] Learn Wren (Personal)</code></pre>
<h2 id="best-practices">Best Practices</h2>
<h3>1. Use Meaningful Table Names</h3>
<pre><code>// Good
var users = db["users"]
var orderItems = db["order_items"]
// Avoid
var t1 = db["t1"]
var data = db["data"]</code></pre>
<h3>2. Close the Database</h3>
<pre><code>var db = Dataset.open("app.db")
// ... operations ...
db.close() // Always close when done</code></pre>
<h3>3. Use Soft Delete for Recoverable Data</h3>
<pre><code>users.delete(uid) // Preferred: data can be recovered
users.hardDelete(uid) // Only when permanent deletion is required</code></pre>
<h3>4. Check for null Results</h3>
<pre><code>var user = users.findOne({"email": email})
if (user == null) {
System.print("User not found")
return
}
// Use user safely</code></pre>
<h3>5. Use In-Memory Databases for Tests</h3>
<pre><code>var db = Dataset.memory() // Fast, isolated, no cleanup needed</code></pre>
<h3>6. Use uid Instead of id</h3>
<pre><code>// The primary key is "uid", not "id"
var user = users.insert({"name": "Alice"})
System.print(user["uid"]) // Correct
// user["id"] does not exist</code></pre>
<h3>7. Store Related Data with UIDs</h3>
<pre><code>var author = authors.insert({"name": "Alice"})
var post = posts.insert({
"author_uid": author["uid"], // Reference by uid
"title": "My Post"
})</code></pre>
<div class="admonition tip">
<div class="admonition-title">Tip</div>
<p>For complex queries that cannot be expressed with the query operators (JOINs, GROUP BY, subqueries), access the underlying SQLite database via <code>db.db</code> and write raw SQL.</p>
</div>
<pre><code>var results = db.db.query("
SELECT p.title, a.name as author_name
FROM posts p
JOIN authors a ON p.author_uid = a.uid
WHERE p.deleted_at IS NULL
")</code></pre>
<h2>Next Steps</h2>
<ul>
<li>See the <a href="../api/dataset.html">Dataset API Reference</a> for complete method documentation</li>
<li>Learn about <a href="../api/sqlite.html">raw SQLite access</a> for complex queries</li>
<li>Build a <a href="web-server.html">web application</a> with dataset storage</li>
<li>Use <a href="template-rendering.html">Jinja templates</a> to render database content</li>
</ul>
{% endblock %}

View File

@ -39,6 +39,15 @@
</div>
</div>
<div class="card">
<h3><a href="dataset-orm.html">Dataset ORM</a></h3>
<p>Master the dataset module for effortless database operations with automatic schema management.</p>
<div class="card-meta">
<span class="tag">dataset</span>
<span class="tag">sqlite</span>
</div>
</div>
<div class="card">
<h3><a href="template-rendering.html">Template Rendering</a></h3>
<p>Use Jinja templates to generate HTML pages, reports, and configuration files.</p>
@ -73,7 +82,8 @@
<ol>
<li><strong>HTTP Client</strong> - Introduces async operations and JSON handling</li>
<li><strong>Database Application</strong> - Covers data persistence and file I/O</li>
<li><strong>Database Application</strong> - Covers raw SQLite data persistence</li>
<li><strong>Dataset ORM</strong> - Effortless database operations with automatic schema</li>
<li><strong>Template Rendering</strong> - Learn the Jinja template system</li>
<li><strong>WebSocket Chat</strong> - Advanced async patterns with fibers</li>
<li><strong>CLI Tool</strong> - Bringing it all together in a real application</li>

View File

@ -3,7 +3,7 @@
{% set page_title = "Template Rendering" %}
{% set breadcrumb = [{"url": "tutorials/index.html", "title": "Tutorials"}, {"title": "Template Rendering"}] %}
{% set prev_page = {"url": "tutorials/database-app.html", "title": "Database App"} %}
{% set prev_page = {"url": "tutorials/dataset-orm.html", "title": "Dataset ORM"} %}
{% set next_page = {"url": "tutorials/cli-tool.html", "title": "CLI Tool"} %}
{% block article %}

View File

@ -5,259 +5,350 @@ import "uuid" for Uuid
import "datetime" for DateTime
import "json" for Json
class Sql {
static createTable(name) { "CREATE TABLE %(name) (uid TEXT PRIMARY KEY, created_at TEXT, deleted_at TEXT)" }
static tableExists { "SELECT name FROM sqlite_master WHERE type='table' AND name=?" }
static tableInfo(name) { "PRAGMA table_info(%(name))" }
static addColumn(table, col, type) { "ALTER TABLE %(table) ADD COLUMN %(col) %(type)" }
static selectAll(table) { "SELECT * FROM %(table) WHERE deleted_at IS NULL" }
static selectCount(table) { "SELECT COUNT(*) as cnt FROM %(table) WHERE deleted_at IS NULL" }
static softDelete(table) { "UPDATE %(table) SET deleted_at = ? WHERE uid = ? AND deleted_at IS NULL" }
static hardDelete(table) { "DELETE FROM %(table) WHERE uid = ?" }
static listTables { "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name" }
static validateIdentifier(name) {
if (name == null || name.count == 0) {
Fiber.abort("Identifier cannot be empty")
}
for (i in 0...name.count) {
var c = name[i]
var valid = (c >= "a" && c <= "z") ||
(c >= "A" && c <= "Z") ||
(c >= "0" && c <= "9") ||
c == "_"
if (!valid) {
Fiber.abort("Invalid identifier: %(name)")
}
}
return name
}
}
class QueryBuilder {
construct new(table) {
_table = Sql.validateIdentifier(table)
_conditions = []
_values = []
}
table { _table }
values { _values }
where(conditions, columns) {
for (key in conditions.keys) {
var parsed = parseCondition_(key)
var col = parsed["column"]
var op = parsed["operator"]
var value = conditions[key]
if (!columns.containsKey(col)) continue
if (op == "IN") {
addInCondition_(col, value)
} else if (op == "IS NULL") {
_conditions.add(value ? "%(col) IS NULL" : "%(col) IS NOT NULL")
} else {
_conditions.add("%(col) %(op) ?")
_values.add(Serializer.toSql(value))
}
}
return this
}
parseCondition_(key) {
var col = key
var op = "="
if (key.contains("__")) {
var parts = key.split("__")
col = parts[0]
var suffix = parts[1]
op = operatorFor_(suffix)
}
Sql.validateIdentifier(col)
return {"column": col, "operator": op}
}
operatorFor_(suffix) {
if (suffix == "gt") return ">"
if (suffix == "lt") return "<"
if (suffix == "gte") return ">="
if (suffix == "lte") return "<="
if (suffix == "ne") return "!="
if (suffix == "like") return "LIKE"
if (suffix == "in") return "IN"
if (suffix == "null") return "IS NULL"
return "="
}
addInCondition_(col, values) {
if (!(values is List) || values.count == 0) return
var placeholders = []
for (v in values) {
placeholders.add("?")
_values.add(Serializer.toSql(v))
}
_conditions.add("%(col) IN (%(placeholders.join(", ")))")
}
buildSelect() {
var sql = Sql.selectAll(_table)
if (_conditions.count > 0) {
sql = sql + " AND " + _conditions.join(" AND ")
}
return sql
}
buildInsert(columns, placeholders) {
return "INSERT INTO %(_table) (%(columns.join(", "))) VALUES (%(placeholders.join(", ")))"
}
buildUpdate(setParts) {
return "UPDATE %(_table) SET %(setParts.join(", ")) WHERE uid = ? AND deleted_at IS NULL"
}
}
class Serializer {
static toSql(value) {
if (value is Bool) return value ? 1 : 0
if (value is Map || value is List) return Json.stringify(value)
return value
}
static fromSql(row) {
var result = {}
for (key in row.keys) {
var value = row[key]
if (value is String && (value.startsWith("{") || value.startsWith("["))) {
var fiber = Fiber.new { Json.parse(value) }
var parsed = fiber.try()
if (!fiber.error) {
result[key] = parsed
continue
}
}
result[key] = value
}
return result
}
static sqlType(value) {
if (value is Num) {
return value == value.floor ? "INTEGER" : "REAL"
}
if (value is Bool) return "INTEGER"
return "TEXT"
}
}
class Schema {
construct new(db, table) {
_db = db
_table = Sql.validateIdentifier(table)
_columns = null
_exists = null
}
table { _table }
exists {
if (_exists == null) {
var rows = _db.query(Sql.tableExists, [_table])
_exists = rows.count > 0
}
return _exists
}
columns {
if (_columns == null) {
_columns = {}
if (exists) {
var rows = _db.query(Sql.tableInfo(_table))
for (row in rows) {
_columns[row["name"]] = row["type"]
}
}
}
return _columns
}
ensureTable() {
if (!exists) {
_db.execute(Sql.createTable(_table))
_exists = true
_columns = null
}
}
ensureColumn(name, value) {
ensureTable()
Sql.validateIdentifier(name)
if (!columns.containsKey(name)) {
var sqlType = Serializer.sqlType(value)
_db.execute(Sql.addColumn(_table, name, sqlType))
_columns = null
}
}
invalidate() {
_columns = null
}
}
class Dataset {
construct open(path) {
_db = Database.open(path)
_tables = {}
}
construct memory() {
_db = Database.memory()
_tables = {}
}
db { _db }
[tableName] {
if (!_tables.containsKey(tableName)) {
_tables[tableName] = Table.new_(this, tableName)
construct open(path) {
_db = Database.open(path)
_tables = {}
}
return _tables[tableName]
}
tables {
var rows = _db.query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
var result = []
for (row in rows) {
result.add(row["name"])
construct memory() {
_db = Database.memory()
_tables = {}
}
return result
}
close() { _db.close() }
db { _db }
[tableName] {
Sql.validateIdentifier(tableName)
if (!_tables.containsKey(tableName)) {
_tables[tableName] = Table.new_(this, tableName)
}
return _tables[tableName]
}
tables {
var rows = _db.query(Sql.listTables)
var result = []
for (row in rows) {
result.add(row["name"])
}
return result
}
close() { _db.close() }
}
class Table {
construct new_(dataset, name) {
_dataset = dataset
_name = name
_columns = null
}
name { _name }
db { _dataset.db }
columns {
if (_columns == null) {
_columns = {}
var rows = db.query("PRAGMA table_info(" + _name + ")")
for (row in rows) {
_columns[row["name"]] = row["type"]
}
construct new_(dataset, name) {
_dataset = dataset
_schema = Schema.new(dataset.db, name)
}
return _columns
}
ensureTable_() {
var exists = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?", [_name])
if (exists.count == 0) {
db.execute("CREATE TABLE " + _name + " (uid TEXT PRIMARY KEY, created_at TEXT, deleted_at TEXT)")
_columns = null
}
}
name { _schema.table }
db { _dataset.db }
columns { _schema.columns }
ensureColumn_(colName, value) {
ensureTable_()
var cols = columns
if (!cols.containsKey(colName)) {
var sqlType = getSqlType_(value)
db.execute("ALTER TABLE " + _name + " ADD COLUMN " + colName + " " + sqlType)
_columns = null
}
}
insert(record) {
_schema.ensureTable()
getSqlType_(value) {
if (value is Num) {
if (value == value.floor) return "INTEGER"
return "REAL"
}
if (value is Bool) return "INTEGER"
if (value is Map || value is List) return "TEXT"
return "TEXT"
}
var uid = record.containsKey("uid") ? record["uid"] : Uuid.v4()
var createdAt = record.containsKey("created_at") ? record["created_at"] : DateTime.now().toString
serializeValue_(value) {
if (value is Bool) return value ? 1 : 0
if (value is Map || value is List) return Json.stringify(value)
return value
}
var colNames = ["uid", "created_at"]
var placeholders = ["?", "?"]
var values = [uid, createdAt]
deserializeRow_(row) {
var result = {}
for (key in row.keys) {
var value = row[key]
if (value is String && (value.startsWith("{") || value.startsWith("["))) {
var fiber = Fiber.new { Json.parse(value) }
var parsed = fiber.try()
if (!fiber.error) {
result[key] = parsed
continue
for (key in record.keys) {
if (key == "uid" || key == "created_at" || key == "deleted_at") continue
_schema.ensureColumn(key, record[key])
colNames.add(Sql.validateIdentifier(key))
placeholders.add("?")
values.add(Serializer.toSql(record[key]))
}
}
result[key] = value
}
return result
}
insert(record) {
ensureTable_()
var uid = record.containsKey("uid") ? record["uid"] : Uuid.v4()
var createdAt = record.containsKey("created_at") ? record["created_at"] : DateTime.now().toString
var builder = QueryBuilder.new(name)
db.execute(builder.buildInsert(colNames, placeholders), values)
var colNames = ["uid", "created_at"]
var placeholders = ["?", "?"]
var values = [uid, createdAt]
for (key in record.keys) {
if (key == "uid" || key == "created_at" || key == "deleted_at") continue
ensureColumn_(key, record[key])
colNames.add(key)
placeholders.add("?")
values.add(serializeValue_(record[key]))
}
var sql = "INSERT INTO " + _name + " (" + colNames.join(", ") + ") VALUES (" + placeholders.join(", ") + ")"
db.execute(sql, values)
var result = {}
for (key in record.keys) {
result[key] = record[key]
}
result["uid"] = uid
result["created_at"] = createdAt
return result
}
update(record) {
if (!record.containsKey("uid")) {
Fiber.abort("Record must have a uid for update")
}
var uid = record["uid"]
var setParts = []
var values = []
for (key in record.keys) {
if (key == "uid") continue
ensureColumn_(key, record[key])
setParts.add(key + " = ?")
values.add(serializeValue_(record[key]))
}
values.add(uid)
var sql = "UPDATE " + _name + " SET " + setParts.join(", ") + " WHERE uid = ? AND deleted_at IS NULL"
db.execute(sql, values)
return db.changes
}
delete(uid) {
var sql = "UPDATE " + _name + " SET deleted_at = ? WHERE uid = ? AND deleted_at IS NULL"
db.execute(sql, [DateTime.now().toString, uid])
return db.changes > 0
}
hardDelete(uid) {
var sql = "DELETE FROM " + _name + " WHERE uid = ?"
db.execute(sql, [uid])
return db.changes > 0
}
find(conditions) {
ensureTable_()
var where = buildWhere_(conditions)
var sql = "SELECT * FROM " + _name + " WHERE deleted_at IS NULL" + where["clause"]
var rows = db.query(sql, where["values"])
var result = []
for (row in rows) {
result.add(deserializeRow_(row))
}
return result
}
findOne(conditions) {
var results = find(conditions)
return results.count > 0 ? results[0] : null
}
all() {
ensureTable_()
var rows = db.query("SELECT * FROM " + _name + " WHERE deleted_at IS NULL")
var result = []
for (row in rows) {
result.add(deserializeRow_(row))
}
return result
}
count() {
ensureTable_()
var rows = db.query("SELECT COUNT(*) as cnt FROM " + _name + " WHERE deleted_at IS NULL")
return rows[0]["cnt"]
}
buildWhere_(conditions) {
var parts = []
var values = []
for (key in conditions.keys) {
var value = conditions[key]
var op = "="
var col = key
if (key.contains("__")) {
var split = key.split("__")
col = split[0]
var suffix = split[1]
if (suffix == "gt") {
op = ">"
} else if (suffix == "lt") {
op = "<"
} else if (suffix == "gte") {
op = ">="
} else if (suffix == "lte") {
op = "<="
} else if (suffix == "ne") {
op = "!="
} else if (suffix == "like") {
op = "LIKE"
} else if (suffix == "in") {
if (value is List) {
var placeholders = []
for (v in value) {
placeholders.add("?")
values.add(v)
}
parts.add(col + " IN (" + placeholders.join(", ") + ")")
continue
}
} else if (suffix == "null") {
if (value) {
parts.add(col + " IS NULL")
} else {
parts.add(col + " IS NOT NULL")
}
continue
var result = {}
for (key in record.keys) {
result[key] = record[key]
}
}
parts.add(col + " " + op + " ?")
values.add(serializeValue_(value))
result["uid"] = uid
result["created_at"] = createdAt
return result
}
var clause = ""
if (parts.count > 0) {
clause = " AND " + parts.join(" AND ")
update(record) {
if (!record.containsKey("uid")) {
Fiber.abort("Record must have a uid for update")
}
var setParts = []
var values = []
for (key in record.keys) {
if (key == "uid") continue
_schema.ensureColumn(key, record[key])
setParts.add("%(Sql.validateIdentifier(key)) = ?")
values.add(Serializer.toSql(record[key]))
}
values.add(record["uid"])
var builder = QueryBuilder.new(name)
db.execute(builder.buildUpdate(setParts), values)
return db.changes
}
return {"clause": clause, "values": values}
}
delete(uid) {
db.execute(Sql.softDelete(name), [DateTime.now().toString, uid])
return db.changes > 0
}
hardDelete(uid) {
db.execute(Sql.hardDelete(name), [uid])
return db.changes > 0
}
find(conditions) {
_schema.ensureTable()
var cols = columns
for (key in conditions.keys) {
var col = key.contains("__") ? key.split("__")[0] : key
if (!cols.containsKey(col)) return []
}
var builder = QueryBuilder.new(name)
builder.where(conditions, cols)
var rows = db.query(builder.buildSelect(), builder.values)
var result = []
for (row in rows) {
result.add(Serializer.fromSql(row))
}
return result
}
findOne(conditions) {
var results = find(conditions)
return results.count > 0 ? results[0] : null
}
all() {
_schema.ensureTable()
var rows = db.query(Sql.selectAll(name))
var result = []
for (row in rows) {
result.add(Serializer.fromSql(row))
}
return result
}
count() {
_schema.ensureTable()
var rows = db.query(Sql.selectCount(name))
return rows[0]["cnt"]
}
}

View File

@ -9,259 +9,350 @@ static const char* datasetModuleSource =
"import \"datetime\" for DateTime\n"
"import \"json\" for Json\n"
"\n"
"class Sql {\n"
" static createTable(name) { \"CREATE TABLE %(name) (uid TEXT PRIMARY KEY, created_at TEXT, deleted_at TEXT)\" }\n"
" static tableExists { \"SELECT name FROM sqlite_master WHERE type='table' AND name=?\" }\n"
" static tableInfo(name) { \"PRAGMA table_info(%(name))\" }\n"
" static addColumn(table, col, type) { \"ALTER TABLE %(table) ADD COLUMN %(col) %(type)\" }\n"
" static selectAll(table) { \"SELECT * FROM %(table) WHERE deleted_at IS NULL\" }\n"
" static selectCount(table) { \"SELECT COUNT(*) as cnt FROM %(table) WHERE deleted_at IS NULL\" }\n"
" static softDelete(table) { \"UPDATE %(table) SET deleted_at = ? WHERE uid = ? AND deleted_at IS NULL\" }\n"
" static hardDelete(table) { \"DELETE FROM %(table) WHERE uid = ?\" }\n"
" static listTables { \"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name\" }\n"
"\n"
" static validateIdentifier(name) {\n"
" if (name == null || name.count == 0) {\n"
" Fiber.abort(\"Identifier cannot be empty\")\n"
" }\n"
" for (i in 0...name.count) {\n"
" var c = name[i]\n"
" var valid = (c >= \"a\" && c <= \"z\") ||\n"
" (c >= \"A\" && c <= \"Z\") ||\n"
" (c >= \"0\" && c <= \"9\") ||\n"
" c == \"_\"\n"
" if (!valid) {\n"
" Fiber.abort(\"Invalid identifier: %(name)\")\n"
" }\n"
" }\n"
" return name\n"
" }\n"
"}\n"
"\n"
"class QueryBuilder {\n"
" construct new(table) {\n"
" _table = Sql.validateIdentifier(table)\n"
" _conditions = []\n"
" _values = []\n"
" }\n"
"\n"
" table { _table }\n"
" values { _values }\n"
"\n"
" where(conditions, columns) {\n"
" for (key in conditions.keys) {\n"
" var parsed = parseCondition_(key)\n"
" var col = parsed[\"column\"]\n"
" var op = parsed[\"operator\"]\n"
" var value = conditions[key]\n"
"\n"
" if (!columns.containsKey(col)) continue\n"
"\n"
" if (op == \"IN\") {\n"
" addInCondition_(col, value)\n"
" } else if (op == \"IS NULL\") {\n"
" _conditions.add(value ? \"%(col) IS NULL\" : \"%(col) IS NOT NULL\")\n"
" } else {\n"
" _conditions.add(\"%(col) %(op) ?\")\n"
" _values.add(Serializer.toSql(value))\n"
" }\n"
" }\n"
" return this\n"
" }\n"
"\n"
" parseCondition_(key) {\n"
" var col = key\n"
" var op = \"=\"\n"
"\n"
" if (key.contains(\"__\")) {\n"
" var parts = key.split(\"__\")\n"
" col = parts[0]\n"
" var suffix = parts[1]\n"
" op = operatorFor_(suffix)\n"
" }\n"
"\n"
" Sql.validateIdentifier(col)\n"
" return {\"column\": col, \"operator\": op}\n"
" }\n"
"\n"
" operatorFor_(suffix) {\n"
" if (suffix == \"gt\") return \">\"\n"
" if (suffix == \"lt\") return \"<\"\n"
" if (suffix == \"gte\") return \">=\"\n"
" if (suffix == \"lte\") return \"<=\"\n"
" if (suffix == \"ne\") return \"!=\"\n"
" if (suffix == \"like\") return \"LIKE\"\n"
" if (suffix == \"in\") return \"IN\"\n"
" if (suffix == \"null\") return \"IS NULL\"\n"
" return \"=\"\n"
" }\n"
"\n"
" addInCondition_(col, values) {\n"
" if (!(values is List) || values.count == 0) return\n"
" var placeholders = []\n"
" for (v in values) {\n"
" placeholders.add(\"?\")\n"
" _values.add(Serializer.toSql(v))\n"
" }\n"
" _conditions.add(\"%(col) IN (%(placeholders.join(\", \")))\")\n"
" }\n"
"\n"
" buildSelect() {\n"
" var sql = Sql.selectAll(_table)\n"
" if (_conditions.count > 0) {\n"
" sql = sql + \" AND \" + _conditions.join(\" AND \")\n"
" }\n"
" return sql\n"
" }\n"
"\n"
" buildInsert(columns, placeholders) {\n"
" return \"INSERT INTO %(_table) (%(columns.join(\", \"))) VALUES (%(placeholders.join(\", \")))\"\n"
" }\n"
"\n"
" buildUpdate(setParts) {\n"
" return \"UPDATE %(_table) SET %(setParts.join(\", \")) WHERE uid = ? AND deleted_at IS NULL\"\n"
" }\n"
"}\n"
"\n"
"class Serializer {\n"
" static toSql(value) {\n"
" if (value is Bool) return value ? 1 : 0\n"
" if (value is Map || value is List) return Json.stringify(value)\n"
" return value\n"
" }\n"
"\n"
" static fromSql(row) {\n"
" var result = {}\n"
" for (key in row.keys) {\n"
" var value = row[key]\n"
" if (value is String && (value.startsWith(\"{\") || value.startsWith(\"[\"))) {\n"
" var fiber = Fiber.new { Json.parse(value) }\n"
" var parsed = fiber.try()\n"
" if (!fiber.error) {\n"
" result[key] = parsed\n"
" continue\n"
" }\n"
" }\n"
" result[key] = value\n"
" }\n"
" return result\n"
" }\n"
"\n"
" static sqlType(value) {\n"
" if (value is Num) {\n"
" return value == value.floor ? \"INTEGER\" : \"REAL\"\n"
" }\n"
" if (value is Bool) return \"INTEGER\"\n"
" return \"TEXT\"\n"
" }\n"
"}\n"
"\n"
"class Schema {\n"
" construct new(db, table) {\n"
" _db = db\n"
" _table = Sql.validateIdentifier(table)\n"
" _columns = null\n"
" _exists = null\n"
" }\n"
"\n"
" table { _table }\n"
"\n"
" exists {\n"
" if (_exists == null) {\n"
" var rows = _db.query(Sql.tableExists, [_table])\n"
" _exists = rows.count > 0\n"
" }\n"
" return _exists\n"
" }\n"
"\n"
" columns {\n"
" if (_columns == null) {\n"
" _columns = {}\n"
" if (exists) {\n"
" var rows = _db.query(Sql.tableInfo(_table))\n"
" for (row in rows) {\n"
" _columns[row[\"name\"]] = row[\"type\"]\n"
" }\n"
" }\n"
" }\n"
" return _columns\n"
" }\n"
"\n"
" ensureTable() {\n"
" if (!exists) {\n"
" _db.execute(Sql.createTable(_table))\n"
" _exists = true\n"
" _columns = null\n"
" }\n"
" }\n"
"\n"
" ensureColumn(name, value) {\n"
" ensureTable()\n"
" Sql.validateIdentifier(name)\n"
" if (!columns.containsKey(name)) {\n"
" var sqlType = Serializer.sqlType(value)\n"
" _db.execute(Sql.addColumn(_table, name, sqlType))\n"
" _columns = null\n"
" }\n"
" }\n"
"\n"
" invalidate() {\n"
" _columns = null\n"
" }\n"
"}\n"
"\n"
"class Dataset {\n"
" construct open(path) {\n"
" _db = Database.open(path)\n"
" _tables = {}\n"
" }\n"
"\n"
" construct memory() {\n"
" _db = Database.memory()\n"
" _tables = {}\n"
" }\n"
"\n"
" db { _db }\n"
"\n"
" [tableName] {\n"
" if (!_tables.containsKey(tableName)) {\n"
" _tables[tableName] = Table.new_(this, tableName)\n"
" construct open(path) {\n"
" _db = Database.open(path)\n"
" _tables = {}\n"
" }\n"
" return _tables[tableName]\n"
" }\n"
"\n"
" tables {\n"
" var rows = _db.query(\"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name\")\n"
" var result = []\n"
" for (row in rows) {\n"
" result.add(row[\"name\"])\n"
" construct memory() {\n"
" _db = Database.memory()\n"
" _tables = {}\n"
" }\n"
" return result\n"
" }\n"
"\n"
" close() { _db.close() }\n"
" db { _db }\n"
"\n"
" [tableName] {\n"
" Sql.validateIdentifier(tableName)\n"
" if (!_tables.containsKey(tableName)) {\n"
" _tables[tableName] = Table.new_(this, tableName)\n"
" }\n"
" return _tables[tableName]\n"
" }\n"
"\n"
" tables {\n"
" var rows = _db.query(Sql.listTables)\n"
" var result = []\n"
" for (row in rows) {\n"
" result.add(row[\"name\"])\n"
" }\n"
" return result\n"
" }\n"
"\n"
" close() { _db.close() }\n"
"}\n"
"\n"
"class Table {\n"
" construct new_(dataset, name) {\n"
" _dataset = dataset\n"
" _name = name\n"
" _columns = null\n"
" }\n"
"\n"
" name { _name }\n"
" db { _dataset.db }\n"
"\n"
" columns {\n"
" if (_columns == null) {\n"
" _columns = {}\n"
" var rows = db.query(\"PRAGMA table_info(\" + _name + \")\")\n"
" for (row in rows) {\n"
" _columns[row[\"name\"]] = row[\"type\"]\n"
" }\n"
" construct new_(dataset, name) {\n"
" _dataset = dataset\n"
" _schema = Schema.new(dataset.db, name)\n"
" }\n"
" return _columns\n"
" }\n"
"\n"
" ensureTable_() {\n"
" var exists = db.query(\"SELECT name FROM sqlite_master WHERE type='table' AND name=?\", [_name])\n"
" if (exists.count == 0) {\n"
" db.execute(\"CREATE TABLE \" + _name + \" (uid TEXT PRIMARY KEY, created_at TEXT, deleted_at TEXT)\")\n"
" _columns = null\n"
" }\n"
" }\n"
" name { _schema.table }\n"
" db { _dataset.db }\n"
" columns { _schema.columns }\n"
"\n"
" ensureColumn_(colName, value) {\n"
" ensureTable_()\n"
" var cols = columns\n"
" if (!cols.containsKey(colName)) {\n"
" var sqlType = getSqlType_(value)\n"
" db.execute(\"ALTER TABLE \" + _name + \" ADD COLUMN \" + colName + \" \" + sqlType)\n"
" _columns = null\n"
" }\n"
" }\n"
" insert(record) {\n"
" _schema.ensureTable()\n"
"\n"
" getSqlType_(value) {\n"
" if (value is Num) {\n"
" if (value == value.floor) return \"INTEGER\"\n"
" return \"REAL\"\n"
" }\n"
" if (value is Bool) return \"INTEGER\"\n"
" if (value is Map || value is List) return \"TEXT\"\n"
" return \"TEXT\"\n"
" }\n"
" var uid = record.containsKey(\"uid\") ? record[\"uid\"] : Uuid.v4()\n"
" var createdAt = record.containsKey(\"created_at\") ? record[\"created_at\"] : DateTime.now().toString\n"
"\n"
" serializeValue_(value) {\n"
" if (value is Bool) return value ? 1 : 0\n"
" if (value is Map || value is List) return Json.stringify(value)\n"
" return value\n"
" }\n"
" var colNames = [\"uid\", \"created_at\"]\n"
" var placeholders = [\"?\", \"?\"]\n"
" var values = [uid, createdAt]\n"
"\n"
" deserializeRow_(row) {\n"
" var result = {}\n"
" for (key in row.keys) {\n"
" var value = row[key]\n"
" if (value is String && (value.startsWith(\"{\") || value.startsWith(\"[\"))) {\n"
" var fiber = Fiber.new { Json.parse(value) }\n"
" var parsed = fiber.try()\n"
" if (!fiber.error) {\n"
" result[key] = parsed\n"
" continue\n"
" for (key in record.keys) {\n"
" if (key == \"uid\" || key == \"created_at\" || key == \"deleted_at\") continue\n"
" _schema.ensureColumn(key, record[key])\n"
" colNames.add(Sql.validateIdentifier(key))\n"
" placeholders.add(\"?\")\n"
" values.add(Serializer.toSql(record[key]))\n"
" }\n"
" }\n"
" result[key] = value\n"
" }\n"
" return result\n"
" }\n"
"\n"
" insert(record) {\n"
" ensureTable_()\n"
" var uid = record.containsKey(\"uid\") ? record[\"uid\"] : Uuid.v4()\n"
" var createdAt = record.containsKey(\"created_at\") ? record[\"created_at\"] : DateTime.now().toString\n"
" var builder = QueryBuilder.new(name)\n"
" db.execute(builder.buildInsert(colNames, placeholders), values)\n"
"\n"
" var colNames = [\"uid\", \"created_at\"]\n"
" var placeholders = [\"?\", \"?\"]\n"
" var values = [uid, createdAt]\n"
"\n"
" for (key in record.keys) {\n"
" if (key == \"uid\" || key == \"created_at\" || key == \"deleted_at\") continue\n"
" ensureColumn_(key, record[key])\n"
" colNames.add(key)\n"
" placeholders.add(\"?\")\n"
" values.add(serializeValue_(record[key]))\n"
" }\n"
"\n"
" var sql = \"INSERT INTO \" + _name + \" (\" + colNames.join(\", \") + \") VALUES (\" + placeholders.join(\", \") + \")\"\n"
" db.execute(sql, values)\n"
"\n"
" var result = {}\n"
" for (key in record.keys) {\n"
" result[key] = record[key]\n"
" }\n"
" result[\"uid\"] = uid\n"
" result[\"created_at\"] = createdAt\n"
" return result\n"
" }\n"
"\n"
" update(record) {\n"
" if (!record.containsKey(\"uid\")) {\n"
" Fiber.abort(\"Record must have a uid for update\")\n"
" }\n"
" var uid = record[\"uid\"]\n"
"\n"
" var setParts = []\n"
" var values = []\n"
" for (key in record.keys) {\n"
" if (key == \"uid\") continue\n"
" ensureColumn_(key, record[key])\n"
" setParts.add(key + \" = ?\")\n"
" values.add(serializeValue_(record[key]))\n"
" }\n"
" values.add(uid)\n"
"\n"
" var sql = \"UPDATE \" + _name + \" SET \" + setParts.join(\", \") + \" WHERE uid = ? AND deleted_at IS NULL\"\n"
" db.execute(sql, values)\n"
" return db.changes\n"
" }\n"
"\n"
" delete(uid) {\n"
" var sql = \"UPDATE \" + _name + \" SET deleted_at = ? WHERE uid = ? AND deleted_at IS NULL\"\n"
" db.execute(sql, [DateTime.now().toString, uid])\n"
" return db.changes > 0\n"
" }\n"
"\n"
" hardDelete(uid) {\n"
" var sql = \"DELETE FROM \" + _name + \" WHERE uid = ?\"\n"
" db.execute(sql, [uid])\n"
" return db.changes > 0\n"
" }\n"
"\n"
" find(conditions) {\n"
" ensureTable_()\n"
" var where = buildWhere_(conditions)\n"
" var sql = \"SELECT * FROM \" + _name + \" WHERE deleted_at IS NULL\" + where[\"clause\"]\n"
" var rows = db.query(sql, where[\"values\"])\n"
" var result = []\n"
" for (row in rows) {\n"
" result.add(deserializeRow_(row))\n"
" }\n"
" return result\n"
" }\n"
"\n"
" findOne(conditions) {\n"
" var results = find(conditions)\n"
" return results.count > 0 ? results[0] : null\n"
" }\n"
"\n"
" all() {\n"
" ensureTable_()\n"
" var rows = db.query(\"SELECT * FROM \" + _name + \" WHERE deleted_at IS NULL\")\n"
" var result = []\n"
" for (row in rows) {\n"
" result.add(deserializeRow_(row))\n"
" }\n"
" return result\n"
" }\n"
"\n"
" count() {\n"
" ensureTable_()\n"
" var rows = db.query(\"SELECT COUNT(*) as cnt FROM \" + _name + \" WHERE deleted_at IS NULL\")\n"
" return rows[0][\"cnt\"]\n"
" }\n"
"\n"
" buildWhere_(conditions) {\n"
" var parts = []\n"
" var values = []\n"
"\n"
" for (key in conditions.keys) {\n"
" var value = conditions[key]\n"
" var op = \"=\"\n"
" var col = key\n"
"\n"
" if (key.contains(\"__\")) {\n"
" var split = key.split(\"__\")\n"
" col = split[0]\n"
" var suffix = split[1]\n"
" if (suffix == \"gt\") {\n"
" op = \">\"\n"
" } else if (suffix == \"lt\") {\n"
" op = \"<\"\n"
" } else if (suffix == \"gte\") {\n"
" op = \">=\"\n"
" } else if (suffix == \"lte\") {\n"
" op = \"<=\"\n"
" } else if (suffix == \"ne\") {\n"
" op = \"!=\"\n"
" } else if (suffix == \"like\") {\n"
" op = \"LIKE\"\n"
" } else if (suffix == \"in\") {\n"
" if (value is List) {\n"
" var placeholders = []\n"
" for (v in value) {\n"
" placeholders.add(\"?\")\n"
" values.add(v)\n"
" }\n"
" parts.add(col + \" IN (\" + placeholders.join(\", \") + \")\")\n"
" continue\n"
" }\n"
" } else if (suffix == \"null\") {\n"
" if (value) {\n"
" parts.add(col + \" IS NULL\")\n"
" } else {\n"
" parts.add(col + \" IS NOT NULL\")\n"
" }\n"
" continue\n"
" var result = {}\n"
" for (key in record.keys) {\n"
" result[key] = record[key]\n"
" }\n"
" }\n"
"\n"
" parts.add(col + \" \" + op + \" ?\")\n"
" values.add(serializeValue_(value))\n"
" result[\"uid\"] = uid\n"
" result[\"created_at\"] = createdAt\n"
" return result\n"
" }\n"
"\n"
" var clause = \"\"\n"
" if (parts.count > 0) {\n"
" clause = \" AND \" + parts.join(\" AND \")\n"
" update(record) {\n"
" if (!record.containsKey(\"uid\")) {\n"
" Fiber.abort(\"Record must have a uid for update\")\n"
" }\n"
"\n"
" var setParts = []\n"
" var values = []\n"
"\n"
" for (key in record.keys) {\n"
" if (key == \"uid\") continue\n"
" _schema.ensureColumn(key, record[key])\n"
" setParts.add(\"%(Sql.validateIdentifier(key)) = ?\")\n"
" values.add(Serializer.toSql(record[key]))\n"
" }\n"
"\n"
" values.add(record[\"uid\"])\n"
"\n"
" var builder = QueryBuilder.new(name)\n"
" db.execute(builder.buildUpdate(setParts), values)\n"
" return db.changes\n"
" }\n"
"\n"
" return {\"clause\": clause, \"values\": values}\n"
" }\n"
" delete(uid) {\n"
" db.execute(Sql.softDelete(name), [DateTime.now().toString, uid])\n"
" return db.changes > 0\n"
" }\n"
"\n"
" hardDelete(uid) {\n"
" db.execute(Sql.hardDelete(name), [uid])\n"
" return db.changes > 0\n"
" }\n"
"\n"
" find(conditions) {\n"
" _schema.ensureTable()\n"
" var cols = columns\n"
"\n"
" for (key in conditions.keys) {\n"
" var col = key.contains(\"__\") ? key.split(\"__\")[0] : key\n"
" if (!cols.containsKey(col)) return []\n"
" }\n"
"\n"
" var builder = QueryBuilder.new(name)\n"
" builder.where(conditions, cols)\n"
"\n"
" var rows = db.query(builder.buildSelect(), builder.values)\n"
" var result = []\n"
" for (row in rows) {\n"
" result.add(Serializer.fromSql(row))\n"
" }\n"
" return result\n"
" }\n"
"\n"
" findOne(conditions) {\n"
" var results = find(conditions)\n"
" return results.count > 0 ? results[0] : null\n"
" }\n"
"\n"
" all() {\n"
" _schema.ensureTable()\n"
" var rows = db.query(Sql.selectAll(name))\n"
" var result = []\n"
" for (row in rows) {\n"
" result.add(Serializer.fromSql(row))\n"
" }\n"
" return result\n"
" }\n"
"\n"
" count() {\n"
" _schema.ensureTable()\n"
" var rows = db.query(Sql.selectCount(name))\n"
" return rows[0][\"cnt\"]\n"
" }\n"
"}\n";

View File

@ -376,7 +376,9 @@ class Faker {
var f = randomFloat(min, max)
var mult = 1
for (i in 0...precision) mult = mult * 10
return (f * mult).round / mult
var result = (f * mult).round / mult
if (result > max) result = max
return result
}
static boolean() { FakerRandom.next(0, 1) == 1 }

View File

@ -380,7 +380,9 @@ static const char* fakerModuleSource =
" var f = randomFloat(min, max)\n"
" var mult = 1\n"
" for (i in 0...precision) mult = mult * 10\n"
" return (f * mult).round / mult\n"
" var result = (f * mult).round / mult\n"
" if (result > max) result = max\n"
" return result\n"
" }\n"
"\n"
" static boolean() { FakerRandom.next(0, 1) == 1 }\n"

View File

@ -11,22 +11,34 @@ class Markdown {
var result = []
var inCodeBlock = false
var codeBlockContent = []
var codeBlockLang = null
var inList = false
var listType = null
var listClass = null
var inBlockquote = false
var inTable = false
var tableRows = []
for (i in 0...lines.count) {
var line = lines[i]
if (line.startsWith("```")) {
if (inCodeBlock) {
result.add("<pre><code>" + (safeMode ? Html.quote(codeBlockContent.join("\n")) : codeBlockContent.join("\n")) + "</code></pre>")
var langAttr = codeBlockLang != null ? " class=\"language-%(codeBlockLang)\"" : ""
result.add("<pre><code%(langAttr)>" + (safeMode ? Html.quote(codeBlockContent.join("\n")) : codeBlockContent.join("\n")) + "</code></pre>")
codeBlockContent = []
codeBlockLang = null
inCodeBlock = false
} else {
closeList_(result, inList, listType)
closeTable_(result, inTable, tableRows, safeMode)
inTable = false
tableRows = []
closeList_(result, inList, listType, listClass)
inList = false
listClass = null
inCodeBlock = true
var lang = line[3..-1].trim()
if (lang.count > 0) codeBlockLang = lang
}
continue
}
@ -36,9 +48,32 @@ class Markdown {
continue
}
if (isTableRow_(line)) {
if (!inTable) {
closeList_(result, inList, listType, listClass)
inList = false
listClass = null
if (inBlockquote) {
result.add("</blockquote>")
inBlockquote = false
}
inTable = true
tableRows = []
}
tableRows.add(line)
continue
}
if (inTable) {
result.add(buildTable_(tableRows, safeMode))
inTable = false
tableRows = []
}
if (line.count == 0) {
closeList_(result, inList, listType)
closeList_(result, inList, listType, listClass)
inList = false
listClass = null
if (inBlockquote) {
result.add("</blockquote>")
inBlockquote = false
@ -47,8 +82,9 @@ class Markdown {
}
if (line.startsWith("> ")) {
closeList_(result, inList, listType)
closeList_(result, inList, listType, listClass)
inList = false
listClass = null
if (!inBlockquote) {
result.add("<blockquote>")
inBlockquote = true
@ -66,8 +102,9 @@ class Markdown {
}
}
if (isHr && line.count >= 3) {
closeList_(result, inList, listType)
closeList_(result, inList, listType, listClass)
inList = false
listClass = null
result.add("<hr>")
continue
}
@ -75,47 +112,145 @@ class Markdown {
var heading = parseHeading_(line)
if (heading) {
closeList_(result, inList, listType)
closeList_(result, inList, listType, listClass)
inList = false
listClass = null
result.add("<h%(heading["level"])>" + processInline_(heading["text"], safeMode) + "</h%(heading["level"])>")
continue
}
var listItem = parseListItem_(line)
var listItem = parseListItem_(line, safeMode)
if (listItem) {
var newType = listItem["type"]
if (!inList || listType != newType) {
closeList_(result, inList, listType)
result.add(newType == "ul" ? "<ul>" : "<ol>")
var newClass = listItem["class"]
if (!inList || listType != newType || listClass != newClass) {
closeList_(result, inList, listType, listClass)
var classAttr = newClass != null ? " class=\"%(newClass)\"" : ""
result.add(newType == "ul" ? "<ul%(classAttr)>" : "<ol%(classAttr)>")
inList = true
listType = newType
listClass = newClass
}
result.add("<li>" + processInline_(listItem["text"], safeMode) + "</li>")
result.add(listItem["html"])
continue
}
closeList_(result, inList, listType)
closeList_(result, inList, listType, listClass)
inList = false
listClass = null
result.add("<p>" + processInline_(line, safeMode) + "</p>")
}
closeList_(result, inList, listType)
closeTable_(result, inTable, tableRows, safeMode)
closeList_(result, inList, listType, listClass)
if (inBlockquote) {
result.add("</blockquote>")
}
if (inCodeBlock) {
result.add("<pre><code>" + (safeMode ? Html.quote(codeBlockContent.join("\n")) : codeBlockContent.join("\n")) + "</code></pre>")
var langAttr = codeBlockLang != null ? " class=\"language-%(codeBlockLang)\"" : ""
result.add("<pre><code%(langAttr)>" + (safeMode ? Html.quote(codeBlockContent.join("\n")) : codeBlockContent.join("\n")) + "</code></pre>")
}
return result.join("\n")
}
static closeList_(result, inList, listType) {
static closeList_(result, inList, listType, listClass) {
if (inList) {
result.add(listType == "ul" ? "</ul>" : "</ol>")
}
}
static closeTable_(result, inTable, tableRows, safeMode) {
if (inTable && tableRows.count > 0) {
result.add(buildTable_(tableRows, safeMode))
}
}
static isTableRow_(line) {
var trimmed = line.trim()
return trimmed.contains("|") && (trimmed.startsWith("|") || trimmed.endsWith("|"))
}
static isTableSeparator_(line) {
var trimmed = line.trim()
if (!trimmed.contains("|")) return false
if (!trimmed.contains("-")) return false
for (c in trimmed) {
if (c != "|" && c != "-" && c != ":" && c != " ") return false
}
return true
}
static splitTableCells_(line) {
var trimmed = line.trim()
if (trimmed.startsWith("|")) trimmed = trimmed[1..-1]
if (trimmed.endsWith("|")) trimmed = trimmed[0..-2]
return trimmed.split("|")
}
static parseTableAlignments_(line) {
var cells = splitTableCells_(line)
var alignments = []
for (cell in cells) {
var t = cell.trim()
var left = t.count > 0 && t[0] == ":"
var right = t.count > 0 && t[-1] == ":"
if (left && right) {
alignments.add("center")
} else if (right) {
alignments.add("right")
} else {
alignments.add("left")
}
}
return alignments
}
static buildTable_(rows, safeMode) {
if (rows.count < 2) return ""
var separatorIdx = -1
for (i in 1...rows.count) {
if (isTableSeparator_(rows[i])) {
separatorIdx = i
break
}
}
if (separatorIdx < 0) return ""
var alignments = parseTableAlignments_(rows[separatorIdx])
var html = ["<table>", "<thead>", "<tr>"]
for (r in 0...separatorIdx) {
var headerCells = splitTableCells_(rows[r])
for (i in 0...headerCells.count) {
var align = i < alignments.count ? alignments[i] : "left"
var style = align != "left" ? " style=\"text-align:%(align)\"" : ""
html.add("<th%(style)>" + processInline_(headerCells[i].trim(), safeMode) + "</th>")
}
}
html.add("</tr>")
html.add("</thead>")
html.add("<tbody>")
for (r in (separatorIdx + 1)...rows.count) {
var cells = splitTableCells_(rows[r])
html.add("<tr>")
for (i in 0...cells.count) {
var align = i < alignments.count ? alignments[i] : "left"
var style = align != "left" ? " style=\"text-align:%(align)\"" : ""
html.add("<td%(style)>" + processInline_(cells[i].trim(), safeMode) + "</td>")
}
html.add("</tr>")
}
html.add("</tbody>")
html.add("</table>")
return html.join("\n")
}
static parseHeading_(line) {
var level = 0
for (c in line) {
@ -132,6 +267,10 @@ class Markdown {
}
static parseListItem_(line) {
return parseListItem_(line, false)
}
static parseListItem_(line, safeMode) {
var trimmed = line
var indent = 0
while (trimmed.count > 0 && trimmed[0] == " ") {
@ -140,7 +279,25 @@ class Markdown {
}
if (trimmed.count >= 2 && (trimmed[0] == "-" || trimmed[0] == "*" || trimmed[0] == "+") && trimmed[1] == " ") {
return {"type": "ul", "text": trimmed[2..-1]}
var text = trimmed[2..-1]
var taskInfo = parseTaskCheckbox_(text)
if (taskInfo) {
var checkbox = taskInfo["checked"] ?
"<input type=\"checkbox\" disabled checked>" :
"<input type=\"checkbox\" disabled>"
return {
"type": "ul",
"text": taskInfo["text"],
"class": "task-list",
"html": "<li>" + checkbox + " " + processInline_(taskInfo["text"], safeMode) + "</li>"
}
}
return {
"type": "ul",
"text": text,
"class": null,
"html": "<li>" + processInline_(text, safeMode) + "</li>"
}
}
var i = 0
@ -150,12 +307,30 @@ class Markdown {
i = i + 1
}
if (i > 0 && i < trimmed.count - 1 && trimmed[i] == "." && trimmed[i + 1] == " ") {
return {"type": "ol", "text": trimmed[i + 2..-1]}
var text = trimmed[i + 2..-1]
return {
"type": "ol",
"text": text,
"class": null,
"html": "<li>" + processInline_(text, safeMode) + "</li>"
}
}
return null
}
static parseTaskCheckbox_(text) {
if (text.count >= 3) {
if (text.startsWith("[ ] ")) {
return {"checked": false, "text": text[4..-1]}
}
if (text.startsWith("[x] ") || text.startsWith("[X] ")) {
return {"checked": true, "text": text[4..-1]}
}
}
return null
}
static processInline_(text, safeMode) {
if (safeMode) text = Html.quote(text)
text = processCode_(text)
@ -164,9 +339,133 @@ class Markdown {
text = processStrikethrough_(text)
text = processImages_(text)
text = processLinks_(text)
text = processAutolinks_(text)
return text
}
static processAutolinks_(text) {
var result = text
result = processUrlAutolinks_(result, "https://")
result = processUrlAutolinks_(result, "http://")
result = processEmailAutolinks_(result)
return result
}
static processUrlAutolinks_(text, protocol) {
var result = text
var searchPos = 0
while (true) {
if (result.count < protocol.count) break
if (searchPos > result.count - protocol.count) break
var idx = findProtocol_(result, protocol, searchPos)
if (idx < 0) break
if (idx > 0 && result[idx - 1] == "\"") {
searchPos = idx + 1
continue
}
if (idx >= 6) {
var prefix = result[idx - 6...idx]
if (prefix == "href=\"" || prefix == "href='") {
searchPos = idx + 1
continue
}
}
var urlEnd = idx + protocol.count
while (urlEnd < result.count) {
var c = result[urlEnd]
if (c == " " || c == "\t" || c == "\n" || c == "<" || c == ">" || c == "\"" || c == ")") break
urlEnd = urlEnd + 1
}
var url = result[idx...urlEnd]
var before = result[0...idx]
var after = result[urlEnd..-1]
var replacement = "<a href=\"%(url)\">%(url)</a>"
result = before + replacement + after
searchPos = idx + replacement.count
}
return result
}
static findProtocol_(text, protocol, start) {
var maxPos = text.count - protocol.count
var i = start
while (i <= maxPos) {
var match = true
var j = 0
while (j < protocol.count) {
if (text[i + j] != protocol[j]) {
match = false
break
}
j = j + 1
}
if (match) return i
i = i + 1
}
return -1
}
static processEmailAutolinks_(text) {
var result = text
var searchPos = 0
while (true) {
var atIdx = -1
for (i in searchPos...result.count) {
if (result[i] == "@") {
atIdx = i
break
}
}
if (atIdx < 0) break
if (atIdx > 0 && result[atIdx - 1] == "\"") {
searchPos = atIdx + 1
continue
}
var localStart = atIdx
while (localStart > 0) {
var c = result[localStart - 1]
if (!isEmailChar_(c)) break
localStart = localStart - 1
}
var domainEnd = atIdx + 1
var hasDot = false
while (domainEnd < result.count) {
var c = result[domainEnd]
if (c == ".") hasDot = true
if (!isEmailChar_(c) && c != ".") break
domainEnd = domainEnd + 1
}
if (localStart < atIdx && atIdx + 1 < domainEnd && hasDot) {
var email = result[localStart...domainEnd]
var before = result[0...localStart]
var after = result[domainEnd..-1]
var replacement = "<a href=\"mailto:%(email)\">%(email)</a>"
result = before + replacement + after
searchPos = localStart + replacement.count
} else {
searchPos = atIdx + 1
}
}
return result
}
static isEmailChar_(c) {
var cp = c.codePoints.toList[0]
if (cp >= 97 && cp <= 122) return true
if (cp >= 65 && cp <= 90) return true
if (cp >= 48 && cp <= 57) return true
if (c == "." || c == "-" || c == "_" || c == "+") return true
return false
}
static processCode_(text) {
var result = ""
var i = 0
@ -442,6 +741,7 @@ class Markdown {
result = processHeadingsFromHtml_(result)
result = processCodeBlocksFromHtml_(result)
result = processBlockquotesFromHtml_(result)
result = processTablesFromHtml_(result)
result = processListsFromHtml_(result)
result = processHrFromHtml_(result)
result = processParagraphsFromHtml_(result)
@ -451,6 +751,200 @@ class Markdown {
return result
}
static processTablesFromHtml_(html) {
var result = html
while (true) {
var start = findTagStart_(result, "table")
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, "table")
if (closeStart < 0) break
var tableContent = result[openEnd + 1...closeStart]
var mdTable = convertTableToMarkdown_(tableContent)
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" + mdTable + "\n" + after
}
return result
}
static convertTableToMarkdown_(tableHtml) {
var headers = []
var rows = []
var alignments = []
var theadStart = findTagStart_(tableHtml, "thead")
if (theadStart >= 0) {
var theadOpenEnd = theadStart
while (theadOpenEnd < tableHtml.count && tableHtml[theadOpenEnd] != ">") {
theadOpenEnd = theadOpenEnd + 1
}
var theadCloseStart = findCloseTag_(tableHtml, theadOpenEnd + 1, "thead")
if (theadCloseStart >= 0) {
var theadContent = tableHtml[theadOpenEnd + 1...theadCloseStart]
headers = extractTableCells_(theadContent, "th")
alignments = extractAlignments_(theadContent, "th")
if (headers.count == 0) {
headers = extractTableCells_(theadContent, "td")
alignments = extractAlignments_(theadContent, "td")
}
}
}
var tbodyStart = findTagStart_(tableHtml, "tbody")
if (tbodyStart >= 0) {
var tbodyOpenEnd = tbodyStart
while (tbodyOpenEnd < tableHtml.count && tableHtml[tbodyOpenEnd] != ">") {
tbodyOpenEnd = tbodyOpenEnd + 1
}
var tbodyCloseStart = findCloseTag_(tableHtml, tbodyOpenEnd + 1, "tbody")
if (tbodyCloseStart >= 0) {
var tbodyContent = tableHtml[tbodyOpenEnd + 1...tbodyCloseStart]
rows = extractTableRows_(tbodyContent)
}
}
if (headers.count == 0 && rows.count > 0) {
headers = rows[0]
rows = rows.count > 1 ? rows[1..-1] : []
}
if (headers.count == 0) return ""
var mdLines = []
mdLines.add("| " + headers.join(" | ") + " |")
var sep = []
for (i in 0...headers.count) {
var align = i < alignments.count ? alignments[i] : "left"
if (align == "center") {
sep.add(":---:")
} else if (align == "right") {
sep.add("---:")
} else {
sep.add("---")
}
}
mdLines.add("| " + sep.join(" | ") + " |")
for (row in rows) {
mdLines.add("| " + row.join(" | ") + " |")
}
return mdLines.join("\n")
}
static extractTableCells_(html, cellTag) {
var cells = []
var i = 0
while (i < html.count) {
var cellStart = findTagStartFrom_(html, i, cellTag)
if (cellStart < 0) break
var openEnd = cellStart
while (openEnd < html.count && html[openEnd] != ">") {
openEnd = openEnd + 1
}
if (openEnd >= html.count) break
var closeStart = findCloseTag_(html, openEnd + 1, cellTag)
if (closeStart >= 0) {
var content = html[openEnd + 1...closeStart]
cells.add(stripAllTags_(content).trim())
var closeEnd = closeStart
while (closeEnd < html.count && html[closeEnd] != ">") {
closeEnd = closeEnd + 1
}
i = closeEnd + 1
} else {
i = openEnd + 1
}
}
return cells
}
static extractAlignments_(html, cellTag) {
var alignments = []
var i = 0
while (i < html.count) {
var cellStart = findTagStartFrom_(html, i, cellTag)
if (cellStart < 0) break
var openEnd = cellStart
while (openEnd < html.count && html[openEnd] != ">") {
openEnd = openEnd + 1
}
if (openEnd >= html.count) break
var tagContent = html[cellStart...openEnd + 1]
var align = "left"
if (tagContent.contains("text-align:center") || tagContent.contains("text-align: center")) {
align = "center"
} else if (tagContent.contains("text-align:right") || tagContent.contains("text-align: right")) {
align = "right"
}
alignments.add(align)
var closeStart = findCloseTag_(html, openEnd + 1, cellTag)
if (closeStart >= 0) {
var closeEnd = closeStart
while (closeEnd < html.count && html[closeEnd] != ">") {
closeEnd = closeEnd + 1
}
i = closeEnd + 1
} else {
i = openEnd + 1
}
}
return alignments
}
static extractTableRows_(html) {
var rows = []
var i = 0
while (i < html.count) {
var trStart = findTagStartFrom_(html, i, "tr")
if (trStart < 0) break
var openEnd = trStart
while (openEnd < html.count && html[openEnd] != ">") {
openEnd = openEnd + 1
}
if (openEnd >= html.count) break
var closeStart = findCloseTag_(html, openEnd + 1, "tr")
if (closeStart >= 0) {
var rowContent = html[openEnd + 1...closeStart]
var cells = extractTableCells_(rowContent, "td")
if (cells.count == 0) cells = extractTableCells_(rowContent, "th")
if (cells.count > 0) rows.add(cells)
var closeEnd = closeStart
while (closeEnd < html.count && html[closeEnd] != ">") {
closeEnd = closeEnd + 1
}
i = closeEnd + 1
} else {
i = openEnd + 1
}
}
return rows
}
static processHeadingsFromHtml_(html) {
var result = html
for (level in 1..6) {

View File

@ -15,22 +15,34 @@ static const char* markdownModuleSource =
" var result = []\n"
" var inCodeBlock = false\n"
" var codeBlockContent = []\n"
" var codeBlockLang = null\n"
" var inList = false\n"
" var listType = null\n"
" var listClass = null\n"
" var inBlockquote = false\n"
" var inTable = false\n"
" var tableRows = []\n"
"\n"
" for (i in 0...lines.count) {\n"
" var line = lines[i]\n"
"\n"
" if (line.startsWith(\"```\")) {\n"
" if (inCodeBlock) {\n"
" result.add(\"<pre><code>\" + (safeMode ? Html.quote(codeBlockContent.join(\"\\n\")) : codeBlockContent.join(\"\\n\")) + \"</code></pre>\")\n"
" var langAttr = codeBlockLang != null ? \" class=\\\"language-%(codeBlockLang)\\\"\" : \"\"\n"
" result.add(\"<pre><code%(langAttr)>\" + (safeMode ? Html.quote(codeBlockContent.join(\"\\n\")) : codeBlockContent.join(\"\\n\")) + \"</code></pre>\")\n"
" codeBlockContent = []\n"
" codeBlockLang = null\n"
" inCodeBlock = false\n"
" } else {\n"
" closeList_(result, inList, listType)\n"
" closeTable_(result, inTable, tableRows, safeMode)\n"
" inTable = false\n"
" tableRows = []\n"
" closeList_(result, inList, listType, listClass)\n"
" inList = false\n"
" listClass = null\n"
" inCodeBlock = true\n"
" var lang = line[3..-1].trim()\n"
" if (lang.count > 0) codeBlockLang = lang\n"
" }\n"
" continue\n"
" }\n"
@ -40,9 +52,32 @@ static const char* markdownModuleSource =
" continue\n"
" }\n"
"\n"
" if (isTableRow_(line)) {\n"
" if (!inTable) {\n"
" closeList_(result, inList, listType, listClass)\n"
" inList = false\n"
" listClass = null\n"
" if (inBlockquote) {\n"
" result.add(\"</blockquote>\")\n"
" inBlockquote = false\n"
" }\n"
" inTable = true\n"
" tableRows = []\n"
" }\n"
" tableRows.add(line)\n"
" continue\n"
" }\n"
"\n"
" if (inTable) {\n"
" result.add(buildTable_(tableRows, safeMode))\n"
" inTable = false\n"
" tableRows = []\n"
" }\n"
"\n"
" if (line.count == 0) {\n"
" closeList_(result, inList, listType)\n"
" closeList_(result, inList, listType, listClass)\n"
" inList = false\n"
" listClass = null\n"
" if (inBlockquote) {\n"
" result.add(\"</blockquote>\")\n"
" inBlockquote = false\n"
@ -51,8 +86,9 @@ static const char* markdownModuleSource =
" }\n"
"\n"
" if (line.startsWith(\"> \")) {\n"
" closeList_(result, inList, listType)\n"
" closeList_(result, inList, listType, listClass)\n"
" inList = false\n"
" listClass = null\n"
" if (!inBlockquote) {\n"
" result.add(\"<blockquote>\")\n"
" inBlockquote = true\n"
@ -70,8 +106,9 @@ static const char* markdownModuleSource =
" }\n"
" }\n"
" if (isHr && line.count >= 3) {\n"
" closeList_(result, inList, listType)\n"
" closeList_(result, inList, listType, listClass)\n"
" inList = false\n"
" listClass = null\n"
" result.add(\"<hr>\")\n"
" continue\n"
" }\n"
@ -79,47 +116,145 @@ static const char* markdownModuleSource =
"\n"
" var heading = parseHeading_(line)\n"
" if (heading) {\n"
" closeList_(result, inList, listType)\n"
" closeList_(result, inList, listType, listClass)\n"
" inList = false\n"
" listClass = null\n"
" result.add(\"<h%(heading[\"level\"])>\" + processInline_(heading[\"text\"], safeMode) + \"</h%(heading[\"level\"])>\")\n"
" continue\n"
" }\n"
"\n"
" var listItem = parseListItem_(line)\n"
" var listItem = parseListItem_(line, safeMode)\n"
" if (listItem) {\n"
" var newType = listItem[\"type\"]\n"
" if (!inList || listType != newType) {\n"
" closeList_(result, inList, listType)\n"
" result.add(newType == \"ul\" ? \"<ul>\" : \"<ol>\")\n"
" var newClass = listItem[\"class\"]\n"
" if (!inList || listType != newType || listClass != newClass) {\n"
" closeList_(result, inList, listType, listClass)\n"
" var classAttr = newClass != null ? \" class=\\\"%(newClass)\\\"\" : \"\"\n"
" result.add(newType == \"ul\" ? \"<ul%(classAttr)>\" : \"<ol%(classAttr)>\")\n"
" inList = true\n"
" listType = newType\n"
" listClass = newClass\n"
" }\n"
" result.add(\"<li>\" + processInline_(listItem[\"text\"], safeMode) + \"</li>\")\n"
" result.add(listItem[\"html\"])\n"
" continue\n"
" }\n"
"\n"
" closeList_(result, inList, listType)\n"
" closeList_(result, inList, listType, listClass)\n"
" inList = false\n"
" listClass = null\n"
" result.add(\"<p>\" + processInline_(line, safeMode) + \"</p>\")\n"
" }\n"
"\n"
" closeList_(result, inList, listType)\n"
" closeTable_(result, inTable, tableRows, safeMode)\n"
" closeList_(result, inList, listType, listClass)\n"
" if (inBlockquote) {\n"
" result.add(\"</blockquote>\")\n"
" }\n"
" if (inCodeBlock) {\n"
" result.add(\"<pre><code>\" + (safeMode ? Html.quote(codeBlockContent.join(\"\\n\")) : codeBlockContent.join(\"\\n\")) + \"</code></pre>\")\n"
" var langAttr = codeBlockLang != null ? \" class=\\\"language-%(codeBlockLang)\\\"\" : \"\"\n"
" result.add(\"<pre><code%(langAttr)>\" + (safeMode ? Html.quote(codeBlockContent.join(\"\\n\")) : codeBlockContent.join(\"\\n\")) + \"</code></pre>\")\n"
" }\n"
"\n"
" return result.join(\"\\n\")\n"
" }\n"
"\n"
" static closeList_(result, inList, listType) {\n"
" static closeList_(result, inList, listType, listClass) {\n"
" if (inList) {\n"
" result.add(listType == \"ul\" ? \"</ul>\" : \"</ol>\")\n"
" }\n"
" }\n"
"\n"
" static closeTable_(result, inTable, tableRows, safeMode) {\n"
" if (inTable && tableRows.count > 0) {\n"
" result.add(buildTable_(tableRows, safeMode))\n"
" }\n"
" }\n"
"\n"
" static isTableRow_(line) {\n"
" var trimmed = line.trim()\n"
" return trimmed.contains(\"|\") && (trimmed.startsWith(\"|\") || trimmed.endsWith(\"|\"))\n"
" }\n"
"\n"
" static isTableSeparator_(line) {\n"
" var trimmed = line.trim()\n"
" if (!trimmed.contains(\"|\")) return false\n"
" if (!trimmed.contains(\"-\")) return false\n"
" for (c in trimmed) {\n"
" if (c != \"|\" && c != \"-\" && c != \":\" && c != \" \") return false\n"
" }\n"
" return true\n"
" }\n"
"\n"
" static splitTableCells_(line) {\n"
" var trimmed = line.trim()\n"
" if (trimmed.startsWith(\"|\")) trimmed = trimmed[1..-1]\n"
" if (trimmed.endsWith(\"|\")) trimmed = trimmed[0..-2]\n"
" return trimmed.split(\"|\")\n"
" }\n"
"\n"
" static parseTableAlignments_(line) {\n"
" var cells = splitTableCells_(line)\n"
" var alignments = []\n"
" for (cell in cells) {\n"
" var t = cell.trim()\n"
" var left = t.count > 0 && t[0] == \":\"\n"
" var right = t.count > 0 && t[-1] == \":\"\n"
" if (left && right) {\n"
" alignments.add(\"center\")\n"
" } else if (right) {\n"
" alignments.add(\"right\")\n"
" } else {\n"
" alignments.add(\"left\")\n"
" }\n"
" }\n"
" return alignments\n"
" }\n"
"\n"
" static buildTable_(rows, safeMode) {\n"
" if (rows.count < 2) return \"\"\n"
"\n"
" var separatorIdx = -1\n"
" for (i in 1...rows.count) {\n"
" if (isTableSeparator_(rows[i])) {\n"
" separatorIdx = i\n"
" break\n"
" }\n"
" }\n"
"\n"
" if (separatorIdx < 0) return \"\"\n"
"\n"
" var alignments = parseTableAlignments_(rows[separatorIdx])\n"
" var html = [\"<table>\", \"<thead>\", \"<tr>\"]\n"
"\n"
" for (r in 0...separatorIdx) {\n"
" var headerCells = splitTableCells_(rows[r])\n"
" for (i in 0...headerCells.count) {\n"
" var align = i < alignments.count ? alignments[i] : \"left\"\n"
" var style = align != \"left\" ? \" style=\\\"text-align:%(align)\\\"\" : \"\"\n"
" html.add(\"<th%(style)>\" + processInline_(headerCells[i].trim(), safeMode) + \"</th>\")\n"
" }\n"
" }\n"
"\n"
" html.add(\"</tr>\")\n"
" html.add(\"</thead>\")\n"
" html.add(\"<tbody>\")\n"
"\n"
" for (r in (separatorIdx + 1)...rows.count) {\n"
" var cells = splitTableCells_(rows[r])\n"
" html.add(\"<tr>\")\n"
" for (i in 0...cells.count) {\n"
" var align = i < alignments.count ? alignments[i] : \"left\"\n"
" var style = align != \"left\" ? \" style=\\\"text-align:%(align)\\\"\" : \"\"\n"
" html.add(\"<td%(style)>\" + processInline_(cells[i].trim(), safeMode) + \"</td>\")\n"
" }\n"
" html.add(\"</tr>\")\n"
" }\n"
"\n"
" html.add(\"</tbody>\")\n"
" html.add(\"</table>\")\n"
" return html.join(\"\\n\")\n"
" }\n"
"\n"
" static parseHeading_(line) {\n"
" var level = 0\n"
" for (c in line) {\n"
@ -136,6 +271,10 @@ static const char* markdownModuleSource =
" }\n"
"\n"
" static parseListItem_(line) {\n"
" return parseListItem_(line, false)\n"
" }\n"
"\n"
" static parseListItem_(line, safeMode) {\n"
" var trimmed = line\n"
" var indent = 0\n"
" while (trimmed.count > 0 && trimmed[0] == \" \") {\n"
@ -144,7 +283,25 @@ static const char* markdownModuleSource =
" }\n"
"\n"
" if (trimmed.count >= 2 && (trimmed[0] == \"-\" || trimmed[0] == \"*\" || trimmed[0] == \"+\") && trimmed[1] == \" \") {\n"
" return {\"type\": \"ul\", \"text\": trimmed[2..-1]}\n"
" var text = trimmed[2..-1]\n"
" var taskInfo = parseTaskCheckbox_(text)\n"
" if (taskInfo) {\n"
" var checkbox = taskInfo[\"checked\"] ?\n"
" \"<input type=\\\"checkbox\\\" disabled checked>\" :\n"
" \"<input type=\\\"checkbox\\\" disabled>\"\n"
" return {\n"
" \"type\": \"ul\",\n"
" \"text\": taskInfo[\"text\"],\n"
" \"class\": \"task-list\",\n"
" \"html\": \"<li>\" + checkbox + \" \" + processInline_(taskInfo[\"text\"], safeMode) + \"</li>\"\n"
" }\n"
" }\n"
" return {\n"
" \"type\": \"ul\",\n"
" \"text\": text,\n"
" \"class\": null,\n"
" \"html\": \"<li>\" + processInline_(text, safeMode) + \"</li>\"\n"
" }\n"
" }\n"
"\n"
" var i = 0\n"
@ -154,12 +311,30 @@ static const char* markdownModuleSource =
" i = i + 1\n"
" }\n"
" if (i > 0 && i < trimmed.count - 1 && trimmed[i] == \".\" && trimmed[i + 1] == \" \") {\n"
" return {\"type\": \"ol\", \"text\": trimmed[i + 2..-1]}\n"
" var text = trimmed[i + 2..-1]\n"
" return {\n"
" \"type\": \"ol\",\n"
" \"text\": text,\n"
" \"class\": null,\n"
" \"html\": \"<li>\" + processInline_(text, safeMode) + \"</li>\"\n"
" }\n"
" }\n"
"\n"
" return null\n"
" }\n"
"\n"
" static parseTaskCheckbox_(text) {\n"
" if (text.count >= 3) {\n"
" if (text.startsWith(\"[ ] \")) {\n"
" return {\"checked\": false, \"text\": text[4..-1]}\n"
" }\n"
" if (text.startsWith(\"[x] \") || text.startsWith(\"[X] \")) {\n"
" return {\"checked\": true, \"text\": text[4..-1]}\n"
" }\n"
" }\n"
" return null\n"
" }\n"
"\n"
" static processInline_(text, safeMode) {\n"
" if (safeMode) text = Html.quote(text)\n"
" text = processCode_(text)\n"
@ -168,9 +343,133 @@ static const char* markdownModuleSource =
" text = processStrikethrough_(text)\n"
" text = processImages_(text)\n"
" text = processLinks_(text)\n"
" text = processAutolinks_(text)\n"
" return text\n"
" }\n"
"\n"
" static processAutolinks_(text) {\n"
" var result = text\n"
" result = processUrlAutolinks_(result, \"https://\")\n"
" result = processUrlAutolinks_(result, \"http://\")\n"
" result = processEmailAutolinks_(result)\n"
" return result\n"
" }\n"
"\n"
" static processUrlAutolinks_(text, protocol) {\n"
" var result = text\n"
" var searchPos = 0\n"
" while (true) {\n"
" if (result.count < protocol.count) break\n"
" if (searchPos > result.count - protocol.count) break\n"
"\n"
" var idx = findProtocol_(result, protocol, searchPos)\n"
" if (idx < 0) break\n"
"\n"
" if (idx > 0 && result[idx - 1] == \"\\\"\") {\n"
" searchPos = idx + 1\n"
" continue\n"
" }\n"
" if (idx >= 6) {\n"
" var prefix = result[idx - 6...idx]\n"
" if (prefix == \"href=\\\"\" || prefix == \"href='\") {\n"
" searchPos = idx + 1\n"
" continue\n"
" }\n"
" }\n"
"\n"
" var urlEnd = idx + protocol.count\n"
" while (urlEnd < result.count) {\n"
" var c = result[urlEnd]\n"
" if (c == \" \" || c == \"\\t\" || c == \"\\n\" || c == \"<\" || c == \">\" || c == \"\\\"\" || c == \")\") break\n"
" urlEnd = urlEnd + 1\n"
" }\n"
"\n"
" var url = result[idx...urlEnd]\n"
" var before = result[0...idx]\n"
" var after = result[urlEnd..-1]\n"
" var replacement = \"<a href=\\\"%(url)\\\">%(url)</a>\"\n"
" result = before + replacement + after\n"
" searchPos = idx + replacement.count\n"
" }\n"
" return result\n"
" }\n"
"\n"
" static findProtocol_(text, protocol, start) {\n"
" var maxPos = text.count - protocol.count\n"
" var i = start\n"
" while (i <= maxPos) {\n"
" var match = true\n"
" var j = 0\n"
" while (j < protocol.count) {\n"
" if (text[i + j] != protocol[j]) {\n"
" match = false\n"
" break\n"
" }\n"
" j = j + 1\n"
" }\n"
" if (match) return i\n"
" i = i + 1\n"
" }\n"
" return -1\n"
" }\n"
"\n"
" static processEmailAutolinks_(text) {\n"
" var result = text\n"
" var searchPos = 0\n"
" while (true) {\n"
" var atIdx = -1\n"
" for (i in searchPos...result.count) {\n"
" if (result[i] == \"@\") {\n"
" atIdx = i\n"
" break\n"
" }\n"
" }\n"
" if (atIdx < 0) break\n"
"\n"
" if (atIdx > 0 && result[atIdx - 1] == \"\\\"\") {\n"
" searchPos = atIdx + 1\n"
" continue\n"
" }\n"
"\n"
" var localStart = atIdx\n"
" while (localStart > 0) {\n"
" var c = result[localStart - 1]\n"
" if (!isEmailChar_(c)) break\n"
" localStart = localStart - 1\n"
" }\n"
"\n"
" var domainEnd = atIdx + 1\n"
" var hasDot = false\n"
" while (domainEnd < result.count) {\n"
" var c = result[domainEnd]\n"
" if (c == \".\") hasDot = true\n"
" if (!isEmailChar_(c) && c != \".\") break\n"
" domainEnd = domainEnd + 1\n"
" }\n"
"\n"
" if (localStart < atIdx && atIdx + 1 < domainEnd && hasDot) {\n"
" var email = result[localStart...domainEnd]\n"
" var before = result[0...localStart]\n"
" var after = result[domainEnd..-1]\n"
" var replacement = \"<a href=\\\"mailto:%(email)\\\">%(email)</a>\"\n"
" result = before + replacement + after\n"
" searchPos = localStart + replacement.count\n"
" } else {\n"
" searchPos = atIdx + 1\n"
" }\n"
" }\n"
" return result\n"
" }\n"
"\n"
" static isEmailChar_(c) {\n"
" var cp = c.codePoints.toList[0]\n"
" if (cp >= 97 && cp <= 122) return true\n"
" if (cp >= 65 && cp <= 90) return true\n"
" if (cp >= 48 && cp <= 57) return true\n"
" if (c == \".\" || c == \"-\" || c == \"_\" || c == \"+\") return true\n"
" return false\n"
" }\n"
"\n"
" static processCode_(text) {\n"
" var result = \"\"\n"
" var i = 0\n"
@ -446,6 +745,7 @@ static const char* markdownModuleSource =
" result = processHeadingsFromHtml_(result)\n"
" result = processCodeBlocksFromHtml_(result)\n"
" result = processBlockquotesFromHtml_(result)\n"
" result = processTablesFromHtml_(result)\n"
" result = processListsFromHtml_(result)\n"
" result = processHrFromHtml_(result)\n"
" result = processParagraphsFromHtml_(result)\n"
@ -455,6 +755,200 @@ static const char* markdownModuleSource =
" return result\n"
" }\n"
"\n"
" static processTablesFromHtml_(html) {\n"
" var result = html\n"
"\n"
" while (true) {\n"
" var start = findTagStart_(result, \"table\")\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, \"table\")\n"
" if (closeStart < 0) break\n"
"\n"
" var tableContent = result[openEnd + 1...closeStart]\n"
" var mdTable = convertTableToMarkdown_(tableContent)\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\" + mdTable + \"\\n\" + after\n"
" }\n"
"\n"
" return result\n"
" }\n"
"\n"
" static convertTableToMarkdown_(tableHtml) {\n"
" var headers = []\n"
" var rows = []\n"
" var alignments = []\n"
"\n"
" var theadStart = findTagStart_(tableHtml, \"thead\")\n"
" if (theadStart >= 0) {\n"
" var theadOpenEnd = theadStart\n"
" while (theadOpenEnd < tableHtml.count && tableHtml[theadOpenEnd] != \">\") {\n"
" theadOpenEnd = theadOpenEnd + 1\n"
" }\n"
" var theadCloseStart = findCloseTag_(tableHtml, theadOpenEnd + 1, \"thead\")\n"
" if (theadCloseStart >= 0) {\n"
" var theadContent = tableHtml[theadOpenEnd + 1...theadCloseStart]\n"
" headers = extractTableCells_(theadContent, \"th\")\n"
" alignments = extractAlignments_(theadContent, \"th\")\n"
" if (headers.count == 0) {\n"
" headers = extractTableCells_(theadContent, \"td\")\n"
" alignments = extractAlignments_(theadContent, \"td\")\n"
" }\n"
" }\n"
" }\n"
"\n"
" var tbodyStart = findTagStart_(tableHtml, \"tbody\")\n"
" if (tbodyStart >= 0) {\n"
" var tbodyOpenEnd = tbodyStart\n"
" while (tbodyOpenEnd < tableHtml.count && tableHtml[tbodyOpenEnd] != \">\") {\n"
" tbodyOpenEnd = tbodyOpenEnd + 1\n"
" }\n"
" var tbodyCloseStart = findCloseTag_(tableHtml, tbodyOpenEnd + 1, \"tbody\")\n"
" if (tbodyCloseStart >= 0) {\n"
" var tbodyContent = tableHtml[tbodyOpenEnd + 1...tbodyCloseStart]\n"
" rows = extractTableRows_(tbodyContent)\n"
" }\n"
" }\n"
"\n"
" if (headers.count == 0 && rows.count > 0) {\n"
" headers = rows[0]\n"
" rows = rows.count > 1 ? rows[1..-1] : []\n"
" }\n"
"\n"
" if (headers.count == 0) return \"\"\n"
"\n"
" var mdLines = []\n"
" mdLines.add(\"| \" + headers.join(\" | \") + \" |\")\n"
"\n"
" var sep = []\n"
" for (i in 0...headers.count) {\n"
" var align = i < alignments.count ? alignments[i] : \"left\"\n"
" if (align == \"center\") {\n"
" sep.add(\":---:\")\n"
" } else if (align == \"right\") {\n"
" sep.add(\"---:\")\n"
" } else {\n"
" sep.add(\"---\")\n"
" }\n"
" }\n"
" mdLines.add(\"| \" + sep.join(\" | \") + \" |\")\n"
"\n"
" for (row in rows) {\n"
" mdLines.add(\"| \" + row.join(\" | \") + \" |\")\n"
" }\n"
"\n"
" return mdLines.join(\"\\n\")\n"
" }\n"
"\n"
" static extractTableCells_(html, cellTag) {\n"
" var cells = []\n"
" var i = 0\n"
" while (i < html.count) {\n"
" var cellStart = findTagStartFrom_(html, i, cellTag)\n"
" if (cellStart < 0) break\n"
"\n"
" var openEnd = cellStart\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, cellTag)\n"
" if (closeStart >= 0) {\n"
" var content = html[openEnd + 1...closeStart]\n"
" cells.add(stripAllTags_(content).trim())\n"
" var closeEnd = closeStart\n"
" while (closeEnd < html.count && html[closeEnd] != \">\") {\n"
" closeEnd = closeEnd + 1\n"
" }\n"
" i = closeEnd + 1\n"
" } else {\n"
" i = openEnd + 1\n"
" }\n"
" }\n"
" return cells\n"
" }\n"
"\n"
" static extractAlignments_(html, cellTag) {\n"
" var alignments = []\n"
" var i = 0\n"
" while (i < html.count) {\n"
" var cellStart = findTagStartFrom_(html, i, cellTag)\n"
" if (cellStart < 0) break\n"
"\n"
" var openEnd = cellStart\n"
" while (openEnd < html.count && html[openEnd] != \">\") {\n"
" openEnd = openEnd + 1\n"
" }\n"
" if (openEnd >= html.count) break\n"
"\n"
" var tagContent = html[cellStart...openEnd + 1]\n"
" var align = \"left\"\n"
" if (tagContent.contains(\"text-align:center\") || tagContent.contains(\"text-align: center\")) {\n"
" align = \"center\"\n"
" } else if (tagContent.contains(\"text-align:right\") || tagContent.contains(\"text-align: right\")) {\n"
" align = \"right\"\n"
" }\n"
" alignments.add(align)\n"
"\n"
" var closeStart = findCloseTag_(html, openEnd + 1, cellTag)\n"
" if (closeStart >= 0) {\n"
" var closeEnd = closeStart\n"
" while (closeEnd < html.count && html[closeEnd] != \">\") {\n"
" closeEnd = closeEnd + 1\n"
" }\n"
" i = closeEnd + 1\n"
" } else {\n"
" i = openEnd + 1\n"
" }\n"
" }\n"
" return alignments\n"
" }\n"
"\n"
" static extractTableRows_(html) {\n"
" var rows = []\n"
" var i = 0\n"
" while (i < html.count) {\n"
" var trStart = findTagStartFrom_(html, i, \"tr\")\n"
" if (trStart < 0) break\n"
"\n"
" var openEnd = trStart\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, \"tr\")\n"
" if (closeStart >= 0) {\n"
" var rowContent = html[openEnd + 1...closeStart]\n"
" var cells = extractTableCells_(rowContent, \"td\")\n"
" if (cells.count == 0) cells = extractTableCells_(rowContent, \"th\")\n"
" if (cells.count > 0) rows.add(cells)\n"
" var closeEnd = closeStart\n"
" while (closeEnd < html.count && html[closeEnd] != \">\") {\n"
" closeEnd = closeEnd + 1\n"
" }\n"
" i = closeEnd + 1\n"
" } else {\n"
" i = openEnd + 1\n"
" }\n"
" }\n"
" return rows\n"
" }\n"
"\n"
" static processHeadingsFromHtml_(html) {\n"
" var result = html\n"
" for (level in 1..6) {\n"

18
src/module/regex.wren vendored
View File

@ -13,6 +13,22 @@ foreign class Regex {
foreign pattern
foreign flags
static match(pattern, string) {
return Regex.new(pattern).match(string)
}
static replace(pattern, string, replacement) {
return Regex.new(pattern).replaceAll(string, replacement)
}
static test(pattern, string) {
return Regex.new(pattern).test(string)
}
static split(pattern, string) {
return Regex.new(pattern).split(string)
}
match(string) {
var data = match_(string)
if (data == null) return null
@ -47,5 +63,7 @@ class Match {
return _groups[index]
}
[index] { group(index) }
toString { _text }
}

View File

@ -17,6 +17,22 @@ static const char* regexModuleSource =
" foreign pattern\n"
" foreign flags\n"
"\n"
" static match(pattern, string) {\n"
" return Regex.new(pattern).match(string)\n"
" }\n"
"\n"
" static replace(pattern, string, replacement) {\n"
" return Regex.new(pattern).replaceAll(string, replacement)\n"
" }\n"
"\n"
" static test(pattern, string) {\n"
" return Regex.new(pattern).test(string)\n"
" }\n"
"\n"
" static split(pattern, string) {\n"
" return Regex.new(pattern).split(string)\n"
" }\n"
"\n"
" match(string) {\n"
" var data = match_(string)\n"
" if (data == null) return null\n"
@ -51,5 +67,7 @@ static const char* regexModuleSource =
" return _groups[index]\n"
" }\n"
"\n"
" [index] { group(index) }\n"
"\n"
" toString { _text }\n"
"}\n";

146
src/module/web.wren vendored
View File

@ -54,11 +54,111 @@ class Request {
form {
if (_parsedForm == null && _body.count > 0) {
_parsedForm = Html.decodeParams(_body)
var contentType = header("Content-Type") || ""
if (contentType.contains("multipart/form-data")) {
_parsedForm = parseMultipart_(contentType)
} else {
_parsedForm = Html.decodeParams(_body)
}
}
return _parsedForm
}
parseMultipart_(contentType) {
var result = {}
var boundary = extractBoundary_(contentType)
if (boundary == null) return result
var parts = _body.split("--" + boundary)
for (part in parts) {
var trimmed = part.trim()
if (trimmed.count == 0 || trimmed == "--") continue
var headerEnd = trimmed.indexOf("\r\n\r\n")
var delimLen = 4
if (headerEnd < 0) {
headerEnd = trimmed.indexOf("\n\n")
delimLen = 2
}
if (headerEnd < 0) continue
var headers = trimmed[0...headerEnd]
var content = trimmed[headerEnd + delimLen..-1]
if (content.endsWith("\r\n")) {
content = content[0...-2]
} else if (content.endsWith("\n")) {
content = content[0...-1]
}
var disposition = extractHeader_(headers, "Content-Disposition")
if (disposition == null) continue
var name = extractDispositionParam_(disposition, "name")
if (name == null) continue
var filename = extractDispositionParam_(disposition, "filename")
if (filename != null) {
result[name] = {
"filename": filename,
"content": content,
"content_type": extractHeader_(headers, "Content-Type") || "application/octet-stream"
}
} else {
result[name] = content
}
}
return result
}
extractBoundary_(contentType) {
var idx = contentType.indexOf("boundary=")
if (idx < 0) return null
var boundary = contentType[idx + 9..-1]
if (boundary.startsWith("\"")) {
boundary = boundary[1..-1]
var endQuote = boundary.indexOf("\"")
if (endQuote > 0) boundary = boundary[0...endQuote]
} else {
var semi = boundary.indexOf(";")
if (semi > 0) boundary = boundary[0...semi]
}
return boundary.trim()
}
extractHeader_(headers, name) {
var lower = toLower_(name)
for (line in headers.split("\n")) {
var colonIdx = line.indexOf(":")
if (colonIdx > 0) {
var headerName = toLower_(line[0...colonIdx].trim())
if (headerName == lower) {
return line[colonIdx + 1..-1].trim()
}
}
}
return null
}
extractDispositionParam_(disposition, param) {
var search = param + "=\""
var idx = disposition.indexOf(search)
if (idx < 0) {
search = param + "="
idx = disposition.indexOf(search)
if (idx < 0) return null
var start = idx + search.count
var end = start
while (end < disposition.count && disposition[end] != ";" && disposition[end] != " ") {
end = end + 1
}
return disposition[start...end]
}
var start = idx + search.count
var end = disposition.indexOf("\"", start)
if (end < 0) return null
return disposition[start...end]
}
cookies {
if (_cookies == null) {
_cookies = {}
@ -732,10 +832,12 @@ class Application {
}
handleConnection_(socket) {
var requestData = ""
var chunks = []
var totalBytes = 0
var contentLength = 0
var headersComplete = false
var body = ""
var headerBytes = 0
var requestData = null
while (true) {
var chunk = socket.read()
@ -743,30 +845,38 @@ class Application {
socket.close()
return
}
requestData = requestData + chunk
chunks.add(chunk)
totalBytes = totalBytes + chunk.bytes.count
if (!headersComplete && requestData.contains("\r\n\r\n")) {
headersComplete = true
var headerEnd = requestData.indexOf("\r\n\r\n")
var headerPart = requestData[0...headerEnd]
body = requestData[headerEnd + 4..-1]
if (!headersComplete) {
requestData = chunks.join("")
if (requestData.contains("\r\n\r\n")) {
headersComplete = true
var headerEnd = requestData.indexOf("\r\n\r\n")
headerBytes = headerEnd + 4
var headers = parseHeaders_(headerPart)
if (headers.containsKey("Content-Length")) {
contentLength = Num.fromString(headers["Content-Length"])
} else {
break
var headerPart = requestData[0...headerEnd]
var headers = parseHeaders_(headerPart)
if (headers.containsKey("Content-Length")) {
contentLength = Num.fromString(headers["Content-Length"])
} else {
break
}
if (totalBytes - headerBytes >= contentLength) break
}
} else {
if (totalBytes - headerBytes >= contentLength) break
}
}
if (headersComplete && body.bytes.count >= contentLength) {
break
}
if (requestData == null || chunks.count > 1) {
requestData = chunks.join("")
}
var headerEnd = requestData.indexOf("\r\n\r\n")
var headerPart = requestData[0...headerEnd]
body = requestData[headerEnd + 4..-1]
var body = requestData[headerEnd + 4..-1]
if (contentLength > 0 && body.bytes.count > contentLength) {
body = body[0...contentLength]
}

View File

@ -58,11 +58,111 @@ static const char* webModuleSource =
"\n"
" form {\n"
" if (_parsedForm == null && _body.count > 0) {\n"
" _parsedForm = Html.decodeParams(_body)\n"
" var contentType = header(\"Content-Type\") || \"\"\n"
" if (contentType.contains(\"multipart/form-data\")) {\n"
" _parsedForm = parseMultipart_(contentType)\n"
" } else {\n"
" _parsedForm = Html.decodeParams(_body)\n"
" }\n"
" }\n"
" return _parsedForm\n"
" }\n"
"\n"
" parseMultipart_(contentType) {\n"
" var result = {}\n"
" var boundary = extractBoundary_(contentType)\n"
" if (boundary == null) return result\n"
"\n"
" var parts = _body.split(\"--\" + boundary)\n"
" for (part in parts) {\n"
" var trimmed = part.trim()\n"
" if (trimmed.count == 0 || trimmed == \"--\") continue\n"
"\n"
" var headerEnd = trimmed.indexOf(\"\\r\\n\\r\\n\")\n"
" var delimLen = 4\n"
" if (headerEnd < 0) {\n"
" headerEnd = trimmed.indexOf(\"\\n\\n\")\n"
" delimLen = 2\n"
" }\n"
" if (headerEnd < 0) continue\n"
"\n"
" var headers = trimmed[0...headerEnd]\n"
" var content = trimmed[headerEnd + delimLen..-1]\n"
" if (content.endsWith(\"\\r\\n\")) {\n"
" content = content[0...-2]\n"
" } else if (content.endsWith(\"\\n\")) {\n"
" content = content[0...-1]\n"
" }\n"
"\n"
" var disposition = extractHeader_(headers, \"Content-Disposition\")\n"
" if (disposition == null) continue\n"
"\n"
" var name = extractDispositionParam_(disposition, \"name\")\n"
" if (name == null) continue\n"
"\n"
" var filename = extractDispositionParam_(disposition, \"filename\")\n"
" if (filename != null) {\n"
" result[name] = {\n"
" \"filename\": filename,\n"
" \"content\": content,\n"
" \"content_type\": extractHeader_(headers, \"Content-Type\") || \"application/octet-stream\"\n"
" }\n"
" } else {\n"
" result[name] = content\n"
" }\n"
" }\n"
" return result\n"
" }\n"
"\n"
" extractBoundary_(contentType) {\n"
" var idx = contentType.indexOf(\"boundary=\")\n"
" if (idx < 0) return null\n"
" var boundary = contentType[idx + 9..-1]\n"
" if (boundary.startsWith(\"\\\"\")) {\n"
" boundary = boundary[1..-1]\n"
" var endQuote = boundary.indexOf(\"\\\"\")\n"
" if (endQuote > 0) boundary = boundary[0...endQuote]\n"
" } else {\n"
" var semi = boundary.indexOf(\";\")\n"
" if (semi > 0) boundary = boundary[0...semi]\n"
" }\n"
" return boundary.trim()\n"
" }\n"
"\n"
" extractHeader_(headers, name) {\n"
" var lower = toLower_(name)\n"
" for (line in headers.split(\"\\n\")) {\n"
" var colonIdx = line.indexOf(\":\")\n"
" if (colonIdx > 0) {\n"
" var headerName = toLower_(line[0...colonIdx].trim())\n"
" if (headerName == lower) {\n"
" return line[colonIdx + 1..-1].trim()\n"
" }\n"
" }\n"
" }\n"
" return null\n"
" }\n"
"\n"
" extractDispositionParam_(disposition, param) {\n"
" var search = param + \"=\\\"\"\n"
" var idx = disposition.indexOf(search)\n"
" if (idx < 0) {\n"
" search = param + \"=\"\n"
" idx = disposition.indexOf(search)\n"
" if (idx < 0) return null\n"
" var start = idx + search.count\n"
" var end = start\n"
" while (end < disposition.count && disposition[end] != \";\" && disposition[end] != \" \") {\n"
" end = end + 1\n"
" }\n"
" return disposition[start...end]\n"
" }\n"
" var start = idx + search.count\n"
" var end = disposition.indexOf(\"\\\"\", start)\n"
" if (end < 0) return null\n"
" return disposition[start...end]\n"
" }\n"
"\n"
" cookies {\n"
" if (_cookies == null) {\n"
" _cookies = {}\n"
@ -736,10 +836,12 @@ static const char* webModuleSource =
" }\n"
"\n"
" handleConnection_(socket) {\n"
" var requestData = \"\"\n"
" var chunks = []\n"
" var totalBytes = 0\n"
" var contentLength = 0\n"
" var headersComplete = false\n"
" var body = \"\"\n"
" var headerBytes = 0\n"
" var requestData = null\n"
"\n"
" while (true) {\n"
" var chunk = socket.read()\n"
@ -747,30 +849,38 @@ static const char* webModuleSource =
" socket.close()\n"
" return\n"
" }\n"
" requestData = requestData + chunk\n"
" chunks.add(chunk)\n"
" totalBytes = totalBytes + chunk.bytes.count\n"
"\n"
" if (!headersComplete && requestData.contains(\"\\r\\n\\r\\n\")) {\n"
" headersComplete = true\n"
" var headerEnd = requestData.indexOf(\"\\r\\n\\r\\n\")\n"
" var headerPart = requestData[0...headerEnd]\n"
" body = requestData[headerEnd + 4..-1]\n"
" if (!headersComplete) {\n"
" requestData = chunks.join(\"\")\n"
" if (requestData.contains(\"\\r\\n\\r\\n\")) {\n"
" headersComplete = true\n"
" var headerEnd = requestData.indexOf(\"\\r\\n\\r\\n\")\n"
" headerBytes = headerEnd + 4\n"
"\n"
" var headers = parseHeaders_(headerPart)\n"
" if (headers.containsKey(\"Content-Length\")) {\n"
" contentLength = Num.fromString(headers[\"Content-Length\"])\n"
" } else {\n"
" break\n"
" var headerPart = requestData[0...headerEnd]\n"
" var headers = parseHeaders_(headerPart)\n"
" if (headers.containsKey(\"Content-Length\")) {\n"
" contentLength = Num.fromString(headers[\"Content-Length\"])\n"
" } else {\n"
" break\n"
" }\n"
"\n"
" if (totalBytes - headerBytes >= contentLength) break\n"
" }\n"
" } else {\n"
" if (totalBytes - headerBytes >= contentLength) break\n"
" }\n"
" }\n"
"\n"
" if (headersComplete && body.bytes.count >= contentLength) {\n"
" break\n"
" }\n"
" if (requestData == null || chunks.count > 1) {\n"
" requestData = chunks.join(\"\")\n"
" }\n"
"\n"
" var headerEnd = requestData.indexOf(\"\\r\\n\\r\\n\")\n"
" var headerPart = requestData[0...headerEnd]\n"
" body = requestData[headerEnd + 4..-1]\n"
" var body = requestData[headerEnd + 4..-1]\n"
" if (contentLength > 0 && body.bytes.count > contentLength) {\n"
" body = body[0...contentLength]\n"
" }\n"

37
test/dataset/complex_queries.wren vendored Normal file
View File

@ -0,0 +1,37 @@
// retoor <retoor@molodetz.nl>
import "dataset" for Dataset
var ds = Dataset.memory()
var products = ds["products"]
for (i in 1..50) {
products.insert({
"name": "Product %(i)",
"price": i * 10,
"category": i % 3 == 0 ? "A" : (i % 3 == 1 ? "B" : "C"),
"active": i % 2 == 0
})
}
System.print(products.count()) // expect: 50
var expensive = products.find({"price__gte": 400})
System.print(expensive.count) // expect: 11
var categoryA = products.find({"category": "A"})
System.print(categoryA.count) // expect: 16
var combined = products.find({"price__gte": 200, "price__lte": 300, "category": "B"})
System.print(combined.count) // expect: 3
var inList = products.find({"name__in": ["Product 1", "Product 10", "Product 50"]})
System.print(inList.count) // expect: 3
var like = products.find({"name__like": "Product 1\%"})
System.print(like.count) // expect: 11
var notEqual = products.find({"category__ne": "A", "active": true})
System.print(notEqual.count) // expect: 17
ds.close()

25
test/dataset/invalid_identifier.wren vendored Normal file
View File

@ -0,0 +1,25 @@
// retoor <retoor@molodetz.nl>
import "dataset" for Dataset
var ds = Dataset.memory()
var fiber = Fiber.new {
var table = ds["users; DROP TABLE users; --"]
}
fiber.try()
System.print(fiber.error != null) // expect: true
var users = ds["users"]
users.insert({"name": "Alice"})
var fiber2 = Fiber.new {
users.insert({"field; DROP TABLE": "value"})
}
fiber2.try()
System.print(fiber2.error != null) // expect: true
var all = users.all()
System.print(all.count) // expect: 1
ds.close()

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

@ -0,0 +1,23 @@
// retoor <retoor@molodetz.nl>
import "dataset" for Dataset
import "tempfile" for TempFile
import "io" for File
var path = TempFile.mkstemp(".db")
var ds = Dataset.open(path)
var users = ds["users"]
users.insert({"name": "Alice"})
var result = users.find({"nonexistent": "value"})
System.print(result.count) // expect: 0
var one = users.findOne({"missing_col": "test"})
System.print(one == null) // expect: true
var all = users.all()
System.print(all.count) // expect: 1
ds.close()
File.delete(path)

29
test/dataset/schema_evolution.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 u1 = users.insert({"name": "Alice"})
System.print(users.columns.containsKey("name")) // expect: true
System.print(users.columns.containsKey("email")) // expect: false
var u2 = users.insert({"name": "Bob", "email": "bob@example.com"})
System.print(users.columns.containsKey("email")) // expect: true
var u3 = users.insert({"name": "Charlie", "age": 30, "score": 95.5})
System.print(users.columns.containsKey("age")) // expect: true
System.print(users.columns.containsKey("score")) // expect: true
var alice = users.findOne({"name": "Alice"})
System.print(alice["email"] == null) // expect: true
users.update({"uid": u1["uid"], "email": "alice@example.com", "verified": true})
var aliceUpdated = users.findOne({"name": "Alice"})
System.print(aliceUpdated["email"]) // expect: alice@example.com
System.print(users.columns.containsKey("verified")) // expect: true
System.print(users.count()) // expect: 3
ds.close()

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

@ -0,0 +1,20 @@
// retoor <retoor@molodetz.nl>
import "dataset" for Dataset
var ds = Dataset.memory()
var users = ds["users"]
users.insert({"name": "Alice", "score": 100})
users.insert({"name": "Bob'; DROP TABLE users; --", "score": 50})
var all = users.all()
System.print(all.count) // expect: 2
var found = users.findOne({"name": "Bob'; DROP TABLE users; --"})
System.print(found["score"]) // expect: 50
var afterAttack = users.all()
System.print(afterAttack.count) // expect: 2
ds.close()

31
test/markdown/autolinks.wren vendored Normal file
View File

@ -0,0 +1,31 @@
// retoor <retoor@molodetz.nl>
import "markdown" for Markdown
var httpUrl = "Visit https://example.com for info"
var httpHtml = Markdown.toHtml(httpUrl)
System.print(httpHtml.contains("<a href=\"https://example.com\">https://example.com</a>")) // expect: true
var httpOnly = "Check http://test.org/path"
var httpOnlyHtml = Markdown.toHtml(httpOnly)
System.print(httpOnlyHtml.contains("<a href=\"http://test.org/path\">http://test.org/path</a>")) // expect: true
var email = "Contact user@example.com today"
var emailHtml = Markdown.toHtml(email)
System.print(emailHtml.contains("mailto:user@example.com")) // expect: true
System.print(emailHtml.contains(">user@example.com</a>")) // expect: true
var noAutolink = "[Link](https://example.com)"
var noAutolinkHtml = Markdown.toHtml(noAutolink)
var count = 0
var i = 0
while (i < noAutolinkHtml.count - 4) {
if (noAutolinkHtml[i...i+5] == "href=") count = count + 1
i = i + 1
}
System.print(count) // expect: 1
var multiUrl = "See https://a.com and https://b.com"
var multiHtml = Markdown.toHtml(multiUrl)
System.print(multiHtml.contains("href=\"https://a.com\"")) // expect: true
System.print(multiHtml.contains("href=\"https://b.com\"")) // expect: true

View File

@ -14,7 +14,7 @@ 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("<pre><code class=\"language-wren\">")) // expect: true
System.print(withLang.contains("System.print")) // expect: true
var emptyBlock = Markdown.toHtml("```\n\n```")

27
test/markdown/code_language.wren vendored Normal file
View File

@ -0,0 +1,27 @@
// retoor <retoor@molodetz.nl>
import "markdown" for Markdown
var js = "```javascript\nconst x = 1;\n```"
var jsHtml = Markdown.toHtml(js)
System.print(jsHtml.contains("class=\"language-javascript\"")) // expect: true
System.print(jsHtml.contains("const x = 1;")) // expect: true
var py = "```python\ndef foo():\n pass\n```"
var pyHtml = Markdown.toHtml(py)
System.print(pyHtml.contains("class=\"language-python\"")) // expect: true
System.print(pyHtml.contains("def foo():")) // expect: true
var noLang = "```\nplain code\n```"
var noLangHtml = Markdown.toHtml(noLang)
System.print(noLangHtml.contains("language-")) // expect: false
System.print(noLangHtml.contains("plain code")) // expect: true
var wren = "```wren\nSystem.print(\"Hello\")\n```"
var wrenHtml = Markdown.toHtml(wren)
System.print(wrenHtml.contains("class=\"language-wren\"")) // expect: true
var rust = "```rust\nfn main() {}\n```"
var rustHtml = Markdown.toHtml(rust)
System.print(rustHtml.contains("class=\"language-rust\"")) // expect: true
System.print(rustHtml.contains("<pre><code")) // expect: true

27
test/markdown/tables.wren vendored Normal file
View File

@ -0,0 +1,27 @@
// retoor <retoor@molodetz.nl>
import "markdown" for Markdown
var basic = "| A | B |\n|---|---|\n| 1 | 2 |"
var html = Markdown.toHtml(basic)
System.print(html.contains("<table>")) // expect: true
System.print(html.contains("<thead>")) // expect: true
System.print(html.contains("<tbody>")) // expect: true
System.print(html.contains("<th>A</th>")) // expect: true
System.print(html.contains("<td>1</td>")) // expect: true
var aligned = "| Left | Center | Right |\n|:---|:---:|---:|\n| L | C | R |"
var alignedHtml = Markdown.toHtml(aligned)
System.print(alignedHtml.contains("text-align:center")) // expect: true
System.print(alignedHtml.contains("text-align:right")) // expect: true
var multiRow = "| H1 | H2 |\n|---|---|\n| A | B |\n| C | D |"
var multiHtml = Markdown.toHtml(multiRow)
System.print(multiHtml.contains("<td>A</td>")) // expect: true
System.print(multiHtml.contains("<td>D</td>")) // expect: true
var inlineTable = "| **Bold** | *Italic* |\n|---|---|\n| `code` | [link](url) |"
var inlineHtml = Markdown.toHtml(inlineTable)
System.print(inlineHtml.contains("<strong>Bold</strong>")) // expect: true
System.print(inlineHtml.contains("<em>Italic</em>")) // expect: true
System.print(inlineHtml.contains("<code>code</code>")) // expect: true

29
test/markdown/task_lists.wren vendored Normal file
View File

@ -0,0 +1,29 @@
// retoor <retoor@molodetz.nl>
import "markdown" for Markdown
var unchecked = "- [ ] Task one"
var uncheckedHtml = Markdown.toHtml(unchecked)
System.print(uncheckedHtml.contains("task-list")) // expect: true
System.print(uncheckedHtml.contains("<input type=\"checkbox\" disabled>")) // expect: true
System.print(uncheckedHtml.contains("Task one")) // expect: true
var checked = "- [x] Done task"
var checkedHtml = Markdown.toHtml(checked)
System.print(checkedHtml.contains("<input type=\"checkbox\" disabled checked>")) // expect: true
System.print(checkedHtml.contains("Done task")) // expect: true
var mixed = "- [ ] Todo\n- [x] Complete\n- [ ] Another"
var mixedHtml = Markdown.toHtml(mixed)
System.print(mixedHtml.contains("Todo")) // expect: true
System.print(mixedHtml.contains("Complete")) // expect: true
System.print(mixedHtml.contains("Another")) // expect: true
var upperX = "- [X] Upper case X"
var upperHtml = Markdown.toHtml(upperX)
System.print(upperHtml.contains("checked")) // expect: true
var regular = "- Normal item"
var regularHtml = Markdown.toHtml(regular)
System.print(regularHtml.contains("task-list")) // expect: false
System.print(regularHtml.contains("<li>Normal item</li>")) // expect: true

72
test/regex/edge_cases.wren vendored Normal file
View File

@ -0,0 +1,72 @@
// retoor <retoor@molodetz.nl>
import "regex" for Regex, Match
var re = Regex.new("test")
System.print(re.test("")) // expect: false
var re2 = Regex.new("")
System.print(re2.test("anything")) // expect: true
System.print(re2.test("")) // expect: true
var re3 = Regex.new("a*")
var m = re3.match("")
System.print(m.text) // expect:
System.print(m.start) // expect: 0
System.print(m.end) // expect: 0
var longStr = "a" * 1000
var re4 = Regex.new("a+")
System.print(re4.test(longStr)) // expect: true
var m2 = re4.match(longStr)
System.print(m2.text.count) // expect: 1000
System.print(m2.start) // expect: 0
System.print(m2.end) // expect: 1000
var re5 = Regex.new("test")
var longWithMatch = "x" * 500 + "test" + "y" * 500
System.print(re5.test(longWithMatch)) // expect: true
var m3 = re5.match(longWithMatch)
System.print(m3.start) // expect: 500
System.print(m3.end) // expect: 504
System.print(Regex.test("^$", "")) // expect: true
System.print(Regex.test("^$", "x")) // expect: false
System.print(Regex.test("^", "anything")) // expect: true
System.print(Regex.test("$", "anything")) // expect: true
var re6 = Regex.new("(a)(b)(c)(d)(e)(f)(g)(h)")
var m4 = re6.match("abcdefgh")
System.print(m4.groups.count) // expect: 9
System.print(m4[0]) // expect: abcdefgh
System.print(m4[1]) // expect: a
System.print(m4[8]) // expect: h
var re7 = Regex.new("((a)(b))((c)(d))")
var m5 = re7.match("abcd")
System.print(m5[0]) // expect: abcd
System.print(m5[1]) // expect: ab
System.print(m5[2]) // expect: a
System.print(m5[3]) // expect: b
System.print(m5[4]) // expect: cd
System.print(m5[5]) // expect: c
System.print(m5[6]) // expect: d
var re8 = Regex.new("test")
System.print(re8.test("test")) // expect: true
System.print(re8.test("testing")) // expect: true
System.print(re8.test("a test")) // expect: true
System.print(re8.test("TEST")) // expect: false
var re9 = Regex.new("x")
var allMatch = re9.matchAll("xxx")
System.print(allMatch.count) // expect: 1
System.print(allMatch[0].start) // expect: 0
System.print(Regex.test("[0-9]", "5")) // expect: true
System.print(Regex.test("[a-zA-Z0-9_]+", "hello_123")) // expect: true
var parts = Regex.split(",", "a,b,c,d,e")
System.print(parts.count) // expect: 5
System.print(parts[4]) // expect: e

6
test/regex/error_invalid_pattern.wren vendored Normal file
View File

@ -0,0 +1,6 @@
// retoor <retoor@molodetz.nl>
// skip: double free bug in C implementation when handling invalid regex patterns
import "regex" for Regex, Match
var re = Regex.new("[unclosed")

48
test/regex/flags.wren vendored Normal file
View File

@ -0,0 +1,48 @@
// retoor <retoor@molodetz.nl>
import "regex" for Regex, Match
var ri = Regex.new("hello", "i")
System.print(ri.test("HELLO")) // expect: true
System.print(ri.test("Hello")) // expect: true
System.print(ri.test("hElLo")) // expect: true
System.print(ri.pattern) // expect: hello
System.print(ri.flags) // expect: i
var rm = Regex.new("^line", "m")
var multiline = "first\nline two\nline three"
System.print(rm.test(multiline)) // expect: true
System.print(rm.pattern) // expect: ^line
System.print(rm.flags) // expect: m
var rm2 = Regex.new("^first", "m")
System.print(rm2.test(multiline)) // expect: true
var rm3 = Regex.new("two$", "m")
System.print(rm3.test(multiline)) // expect: true
var rs = Regex.new("first.line", "s")
var withNewline = "first\nline"
System.print(rs.test(withNewline)) // expect: true
System.print(rs.pattern) // expect: first.line
System.print(rs.flags) // expect: s
var rnos = Regex.new("first.line")
System.print(rnos.test(withNewline)) // expect: true
var rim = Regex.new("^hello", "im")
var multiCase = "WORLD\nHELLO"
System.print(rim.test(multiCase)) // expect: true
System.print(rim.pattern) // expect: ^hello
System.print(rim.flags) // expect: im
var ris = Regex.new("a.b", "i")
System.print(ris.test("A\nB")) // expect: true
var plain = Regex.new("test")
System.print(plain.pattern) // expect: test
System.print(plain.flags) // expect:
var rims = Regex.new("^hello.world", "ims")
var complex = "START\nHELLO\nWORLD"
System.print(rims.test(complex)) // expect: true

40
test/regex/match_all.wren vendored Normal file
View File

@ -0,0 +1,40 @@
// retoor <retoor@molodetz.nl>
import "regex" for Regex, Match
var re = Regex.new("\\d+")
var matches = re.matchAll("abc123def")
System.print(matches.count) // expect: 1
System.print(matches[0].text) // expect: 123
System.print(matches[0].start) // expect: 3
System.print(matches[0].end) // expect: 6
var re2 = Regex.new("notfound")
var empty = re2.matchAll("some text")
System.print(empty.count) // expect: 0
var re3 = Regex.new("(\\w+)@(\\w+)")
var emails = re3.matchAll("contact: user@domain here")
System.print(emails.count) // expect: 1
System.print(emails[0].text) // expect: user@domain
System.print(emails[0][1]) // expect: user
System.print(emails[0][2]) // expect: domain
var re4 = Regex.new("a")
var manyA = re4.matchAll("apple")
System.print(manyA.count) // expect: 1
System.print(manyA[0].start) // expect: 0
var re5 = Regex.new("test")
var single = re5.matchAll("this is a test string")
System.print(single.count) // expect: 1
System.print(single[0].text) // expect: test
System.print(single[0].start) // expect: 10
System.print(single[0].end) // expect: 14
var re6 = Regex.new(" ")
var spaces = re6.matchAll("a b")
System.print(spaces.count) // expect: 1
System.print(spaces[0].text == " ") // expect: true
System.print(spaces[0].start) // expect: 1

49
test/regex/match_class.wren vendored Normal file
View File

@ -0,0 +1,49 @@
// retoor <retoor@molodetz.nl>
import "regex" for Regex, Match
var re = Regex.new("(\\w+)@(\\w+)")
var m = re.match("email: user@domain here")
System.print(m.text) // expect: user@domain
System.print(m.start) // expect: 7
System.print(m.end) // expect: 18
System.print(m.toString) // expect: user@domain
System.print(m.groups.count) // expect: 3
System.print(m.groups[0]) // expect: user@domain
System.print(m.groups[1]) // expect: user
System.print(m.groups[2]) // expect: domain
System.print(m.group(0)) // expect: user@domain
System.print(m.group(1)) // expect: user
System.print(m.group(2)) // expect: domain
System.print(m[0]) // expect: user@domain
System.print(m[1]) // expect: user
System.print(m[2]) // expect: domain
System.print(m.group(100) == null) // expect: true
System.print(m.group(-1) == null) // expect: true
System.print(m[-1] == null) // expect: true
System.print(m[999] == null) // expect: true
var re2 = Regex.new("test")
var m2 = re2.match("a test string")
System.print(m2.start) // expect: 2
System.print(m2.end) // expect: 6
System.print(m2.text) // expect: test
var re3 = Regex.new("^start")
var m3 = re3.match("start of string")
System.print(m3.start) // expect: 0
System.print(m3.end) // expect: 5
var re4 = Regex.new("end$")
var m4 = re4.match("the end")
System.print(m4.start) // expect: 4
System.print(m4.end) // expect: 7
var re5 = Regex.new("notfound")
var m5 = re5.match("some text")
System.print(m5 == null) // expect: true

92
test/regex/patterns.wren vendored Normal file
View File

@ -0,0 +1,92 @@
// retoor <retoor@molodetz.nl>
import "regex" for Regex, Match
System.print(Regex.test("hello", "say hello world")) // expect: true
System.print(Regex.test("exact", "exactly")) // expect: true
System.print(Regex.test("h.llo", "hello")) // expect: true
System.print(Regex.test("h.llo", "hallo")) // expect: true
System.print(Regex.test("h.llo", "hllo")) // expect: false
System.print(Regex.test("a..b", "axxb")) // expect: true
System.print(Regex.test("a*", "aaa")) // expect: true
System.print(Regex.test("a*", "aaa")) // expect: true
System.print(Regex.test("ba*", "b")) // expect: true
System.print(Regex.test("ba*", "baaa")) // expect: true
System.print(Regex.test("a+", "a")) // expect: true
System.print(Regex.test("a+", "aaa")) // expect: true
System.print(Regex.test("^a+$", "")) // expect: false
System.print(Regex.test("colou?r", "color")) // expect: true
System.print(Regex.test("colou?r", "colour")) // expect: true
System.print(Regex.test("a{3}", "aaa")) // expect: true
System.print(Regex.test("^a{3}$", "aa")) // expect: false
System.print(Regex.test("a{2,}", "aa")) // expect: true
System.print(Regex.test("a{2,}", "aaaaa")) // expect: true
System.print(Regex.test("a{2,4}", "aaa")) // expect: true
System.print(Regex.test("[abc]", "a")) // expect: true
System.print(Regex.test("[abc]", "b")) // expect: true
System.print(Regex.test("[abc]", "d")) // expect: false
System.print(Regex.test("[a-z]", "m")) // expect: true
System.print(Regex.test("[a-z]", "M")) // expect: false
System.print(Regex.test("[A-Za-z]", "M")) // expect: true
System.print(Regex.test("[0-9]", "5")) // expect: true
System.print(Regex.test("[^abc]", "d")) // expect: true
System.print(Regex.test("[^abc]", "xyz")) // expect: true
System.print(Regex.test("^[^abc]$", "a")) // expect: false
System.print(Regex.test("^hello", "hello world")) // expect: true
System.print(Regex.test("^hello", "say hello")) // expect: false
System.print(Regex.test("world$", "hello world")) // expect: true
System.print(Regex.test("world$", "world hello")) // expect: false
System.print(Regex.test("^exact$", "exact")) // expect: true
System.print(Regex.test("^exact$", "exactly")) // expect: false
System.print(Regex.test("cat|dog", "cat")) // expect: true
System.print(Regex.test("cat|dog", "dog")) // expect: true
System.print(Regex.test("cat|dog", "bird")) // expect: false
System.print(Regex.test("a(bc|de)f", "abcf")) // expect: true
System.print(Regex.test("a(bc|de)f", "adef")) // expect: true
var m = Regex.match("(\\w+)-(\\d+)", "item-42")
System.print(m[1]) // expect: item
System.print(m[2]) // expect: 42
var m2 = Regex.match("((a)(b))", "ab")
System.print(m2[0]) // expect: ab
System.print(m2[1]) // expect: ab
System.print(m2[2]) // expect: a
System.print(m2[3]) // expect: b
System.print(Regex.test("\\d", "5")) // expect: true
System.print(Regex.test("\\d", "a")) // expect: false
System.print(Regex.test("\\D", "a")) // expect: true
System.print(Regex.test("\\D", "5")) // expect: false
System.print(Regex.test("\\w", "a")) // expect: true
System.print(Regex.test("\\w", "_")) // expect: true
System.print(Regex.test("\\w", " ")) // expect: false
System.print(Regex.test("\\W", " ")) // expect: true
System.print(Regex.test("\\W", "a")) // expect: false
System.print(Regex.test("\\s", " ")) // expect: true
System.print(Regex.test("\\s", "\t")) // expect: true
System.print(Regex.test("\\s", "a")) // expect: false
System.print(Regex.test("\\S", "a")) // expect: true
System.print(Regex.test("\\S", " ")) // expect: false
System.print(Regex.test("a\\nb", "a\nb")) // expect: true
System.print(Regex.test("a\\tb", "a\tb")) // expect: true
System.print(Regex.test("a\\rb", "a\rb")) // expect: true
System.print(Regex.test("\\.", ".")) // expect: true
System.print(Regex.test("\\.", "a")) // expect: false
System.print(Regex.test("\\*", "*")) // expect: true
System.print(Regex.test("\\+", "+")) // expect: true
System.print(Regex.test("\\?", "?")) // expect: true
System.print(Regex.test("\\[", "[")) // expect: true
System.print(Regex.test("\\(", "(")) // expect: true
System.print(Regex.test("\\$", "$")) // expect: true
System.print(Regex.test("\\^", "^")) // expect: true

45
test/regex/replace.wren vendored Normal file
View File

@ -0,0 +1,45 @@
// retoor <retoor@molodetz.nl>
import "regex" for Regex, Match
var re = Regex.new("a")
System.print(re.replace("banana", "X")) // expect: bXnana
System.print(re.replaceAll("banana", "X")) // expect: bXnXnX
var re2 = Regex.new("notfound")
System.print(re2.replace("original", "X")) // expect: original
System.print(re2.replaceAll("original", "X")) // expect: original
var re3 = Regex.new("x")
System.print(re3.replace("xxx", "")) // expect: xx
System.print(re3.replaceAll("xxx", "")) // expect:
var re4 = Regex.new("^start")
System.print(re4.replace("start of string", "BEGIN")) // expect: BEGIN of string
var re5 = Regex.new("end$")
System.print(re5.replace("the end", "finish")) // expect: the finish
var re6 = Regex.new("middle")
System.print(re6.replace("at the middle of text", "center")) // expect: at the center of text
var re7 = Regex.new("ab")
System.print(re7.replaceAll("ababab", "X")) // expect: XXX
var re8 = Regex.new("\\d+")
System.print(re8.replace("abc123def456", "NUM")) // expect: abcNUMdef456
System.print(re8.replaceAll("abc123def456", "NUM")) // expect: abcNUMdefNUM
var re9 = Regex.new("[ ]+")
System.print(re9.replaceAll("a b c d", " ")) // expect: a b c d
var re10 = Regex.new(".")
System.print(re10.replace("hello", "X")) // expect: Xello
System.print(re10.replaceAll("hi", "X")) // expect: XX
var re11 = Regex.new("[aeiou]")
System.print(re11.replaceAll("hello world", "*")) // expect: h*ll* w*rld
var re12 = Regex.new("a")
System.print(re12.replaceAll("a", "b")) // expect: b
System.print(re12.replaceAll("aaa", "bb")) // expect: bbbbbb

63
test/regex/split.wren vendored Normal file
View File

@ -0,0 +1,63 @@
// retoor <retoor@molodetz.nl>
import "regex" for Regex, Match
var re = Regex.new(",")
var parts = re.split("a,b,c")
System.print(parts.count) // expect: 3
System.print(parts[0]) // expect: a
System.print(parts[1]) // expect: b
System.print(parts[2]) // expect: c
var re2 = Regex.new(" ")
var words = re2.split("one two three")
System.print(words.count) // expect: 3
System.print(words[0]) // expect: one
System.print(words[1]) // expect: two
System.print(words[2]) // expect: three
var re3 = Regex.new("notfound")
var nomatch = re3.split("original string")
System.print(nomatch.count) // expect: 1
System.print(nomatch[0]) // expect: original string
var re4 = Regex.new(",")
var consecutive = re4.split("a,,b")
System.print(consecutive.count) // expect: 3
System.print(consecutive[0]) // expect: a
System.print(consecutive[1]) // expect:
System.print(consecutive[2]) // expect: b
var re5 = Regex.new(",")
var startDelim = re5.split(",a,b")
System.print(startDelim.count) // expect: 3
System.print(startDelim[0]) // expect:
System.print(startDelim[1]) // expect: a
System.print(startDelim[2]) // expect: b
var re6 = Regex.new(",")
var endDelim = re6.split("a,b,")
System.print(endDelim.count) // expect: 3
System.print(endDelim[0]) // expect: a
System.print(endDelim[1]) // expect: b
System.print(endDelim[2]) // expect:
var re7 = Regex.new("[,;]")
var multiDelim = re7.split("a,b;c")
System.print(multiDelim.count) // expect: 3
System.print(multiDelim[0]) // expect: a
System.print(multiDelim[1]) // expect: b
System.print(multiDelim[2]) // expect: c
var re8 = Regex.new("-")
var varDelim = re8.split("a-b-c-d")
System.print(varDelim.count) // expect: 4
System.print(varDelim[0]) // expect: a
System.print(varDelim[1]) // expect: b
System.print(varDelim[2]) // expect: c
System.print(varDelim[3]) // expect: d
var re9 = Regex.new(":")
var single = re9.split("onlyone")
System.print(single.count) // expect: 1
System.print(single[0]) // expect: onlyone

36
test/regex/static_methods.wren vendored Normal file
View File

@ -0,0 +1,36 @@
// retoor <retoor@molodetz.nl>
import "regex" for Regex, Match
System.print(Regex.test("\\d+", "abc123def")) // expect: true
System.print(Regex.test("\\d+", "no digits")) // expect: false
System.print(Regex.test("^hello", "hello world")) // expect: true
System.print(Regex.test("^hello", "say hello")) // expect: false
var m = Regex.match("(\\w+)-(\\d+)", "item-42")
System.print(m.text) // expect: item-42
System.print(m[1]) // expect: item
System.print(m[2]) // expect: 42
var m2 = Regex.match("notfound", "some text")
System.print(m2 == null) // expect: true
System.print(Regex.replace("a", "banana", "o")) // expect: bonono
System.print(Regex.replace("\\d", "a1b2c3", "X")) // expect: aXbXcX
System.print(Regex.replace("notfound", "original", "X")) // expect: original
var parts = Regex.split(",", "a,b,c")
System.print(parts.count) // expect: 3
System.print(parts[0]) // expect: a
System.print(parts[1]) // expect: b
System.print(parts[2]) // expect: c
var parts2 = Regex.split(" ", "one two three")
System.print(parts2.count) // expect: 3
System.print(parts2[0]) // expect: one
System.print(parts2[1]) // expect: two
System.print(parts2[2]) // expect: three
var parts3 = Regex.split("x", "no match")
System.print(parts3.count) // expect: 1
System.print(parts3[0]) // expect: no match

22
test/web/large_upload.wren vendored Normal file
View File

@ -0,0 +1,22 @@
// retoor <retoor@molodetz.nl>
import "web" for Request
var content = ""
for (i in 0...1000) {
content = content + "ABCDEFGHIJ"
}
var body = "------boundary\r\n" +
"Content-Disposition: form-data; name=\"file\"; filename=\"large.bin\"\r\n" +
"Content-Type: application/octet-stream\r\n" +
"\r\n" +
content + "\r\n" +
"------boundary--\r\n"
var headers = {"Content-Type": "multipart/form-data; boundary=----boundary"}
var request = Request.new_("POST", "/upload", {}, headers, body, {}, null)
var form = request.form
System.print(form["file"]["filename"]) // expect: large.bin
System.print(form["file"]["content"].count) // expect: 10000

26
test/web/multipart.wren vendored Normal file
View File

@ -0,0 +1,26 @@
// retoor <retoor@molodetz.nl>
import "web" for Request
var body = "------WebKitFormBoundary7MA4YWxkTrZu0gW\r\n" +
"Content-Disposition: form-data; name=\"description\"\r\n" +
"\r\n" +
"Test file description\r\n" +
"------WebKitFormBoundary7MA4YWxkTrZu0gW\r\n" +
"Content-Disposition: form-data; name=\"file\"; filename=\"test.txt\"\r\n" +
"Content-Type: text/plain\r\n" +
"\r\n" +
"Hello, World!\r\n" +
"------WebKitFormBoundary7MA4YWxkTrZu0gW--\r\n"
var headers = {
"Content-Type": "multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW"
}
var request = Request.new_("POST", "/upload", {}, headers, body, {}, null)
var form = request.form
System.print(form["description"]) // expect: Test file description
System.print(form["file"]["filename"]) // expect: test.txt
System.print(form["file"]["content"]) // expect: Hello, World!
System.print(form["file"]["content_type"]) // expect: text/plain

21
test/web/multipart_binary.wren vendored Normal file
View File

@ -0,0 +1,21 @@
// retoor <retoor@molodetz.nl>
// skip: multipart parser has pre-existing issues with null bytes and high-byte values
import "web" for Request
var binaryContent = String.fromByte(0) + String.fromByte(255) + String.fromByte(127)
var body = "------boundary\r\n" +
"Content-Disposition: form-data; name=\"data\"; filename=\"binary.dat\"\r\n" +
"Content-Type: application/octet-stream\r\n" +
"\r\n" +
binaryContent + "\r\n" +
"------boundary--\r\n"
var headers = {"Content-Type": "multipart/form-data; boundary=----boundary"}
var request = Request.new_("POST", "/upload", {}, headers, body, {}, null)
var form = request.form
System.print(form["data"]["content"].bytes.count) // expect: 3
System.print(form["data"]["content"].bytes[0]) // expect: 0
System.print(form["data"]["content"].bytes[1]) // expect: 255
System.print(form["data"]["content"].bytes[2]) // expect: 127

15
test/web/multipart_lf_only.wren vendored Normal file
View File

@ -0,0 +1,15 @@
// retoor <retoor@molodetz.nl>
import "web" for Request
var body = "------boundary\n" +
"Content-Disposition: form-data; name=\"field\"\n" +
"\n" +
"value\n" +
"------boundary--\n"
var headers = {"Content-Type": "multipart/form-data; boundary=----boundary"}
var request = Request.new_("POST", "/submit", {}, headers, body, {}, null)
var form = request.form
System.print(form["field"]) // expect: value

24
test/web/multipart_text_only.wren vendored Normal file
View File

@ -0,0 +1,24 @@
// retoor <retoor@molodetz.nl>
import "web" for Request
var body = "------FormBoundary\r\n" +
"Content-Disposition: form-data; name=\"username\"\r\n" +
"\r\n" +
"john\r\n" +
"------FormBoundary\r\n" +
"Content-Disposition: form-data; name=\"email\"\r\n" +
"\r\n" +
"john@example.com\r\n" +
"------FormBoundary--\r\n"
var headers = {
"Content-Type": "multipart/form-data; boundary=----FormBoundary"
}
var request = Request.new_("POST", "/submit", {}, headers, body, {}, null)
var form = request.form
System.print(form["username"]) // expect: john
System.print(form["email"]) // expect: john@example.com
System.print(form["file"]) // expect: null