Update.
This commit is contained in:
parent
fe2f087d9f
commit
fbc1a6e294
@ -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"
|
||||
|
||||
@ -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><code></code>, blocks use <code><pre><code></code>.</p>
|
||||
<p>Inline code uses <code><code></code>, blocks use <code><pre><code></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><ul></code> and <code><ol></code> with <code><li></code> items.</p>
|
||||
|
||||
<h3>Task Lists (GFM)</h3>
|
||||
<pre><code>- [ ] Unchecked task
|
||||
- [x] Checked task
|
||||
- [X] Also checked</code></pre>
|
||||
<p>Creates <code><ul class="task-list"></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><thead></code> and <code><tbody></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)
|
||||
</code></pre>
|
||||
@ -213,6 +235,10 @@ ___</code></pre>
|
||||
<td><code><del></code>, <code><s></code></td>
|
||||
<td><code>~~text~~</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code><table></code></td>
|
||||
<td>GFM table syntax</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p>Container tags (<code><div></code>, <code><span></code>, <code><section></code>, etc.) are stripped but their content is preserved. Script and style tags are removed entirely.</p>
|
||||
@ -263,7 +289,7 @@ var page = "<html>
|
||||
|
||||
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)
|
||||
// <pre><code>System.print("Hello, World!")</code></pre></code></pre>
|
||||
// <pre><code class="language-wren">System.print("Hello, World!")</code></pre></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 %}
|
||||
|
||||
@ -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>
|
||||
|
||||
628
manual_src/pages/tutorials/dataset-orm.html
Normal file
628
manual_src/pages/tutorials/dataset-orm.html
Normal 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>></code></td>
|
||||
<td><code>{"age__gt": 18}</code> - age greater than 18</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>__lt</code></td>
|
||||
<td><code><</code></td>
|
||||
<td><code>{"price__lt": 100}</code> - price less than 100</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>__gte</code></td>
|
||||
<td><code>>=</code></td>
|
||||
<td><code>{"score__gte": 90}</code> - score 90 or higher</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>__lte</code></td>
|
||||
<td><code><=</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 %}
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
557
src/module/dataset.wren
vendored
557
src/module/dataset.wren
vendored
@ -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"]
|
||||
}
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
4
src/module/faker.wren
vendored
4
src/module/faker.wren
vendored
@ -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 }
|
||||
|
||||
@ -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"
|
||||
|
||||
528
src/module/markdown.wren
vendored
528
src/module/markdown.wren
vendored
@ -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) {
|
||||
|
||||
@ -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
18
src/module/regex.wren
vendored
@ -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 }
|
||||
}
|
||||
|
||||
@ -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
146
src/module/web.wren
vendored
@ -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]
|
||||
}
|
||||
|
||||
@ -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
37
test/dataset/complex_queries.wren
vendored
Normal 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
25
test/dataset/invalid_identifier.wren
vendored
Normal 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
23
test/dataset/missing_column.wren
vendored
Normal 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
29
test/dataset/schema_evolution.wren
vendored
Normal 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
20
test/dataset/sql_injection.wren
vendored
Normal 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
31
test/markdown/autolinks.wren
vendored
Normal 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
|
||||
2
test/markdown/blocks_advanced.wren
vendored
2
test/markdown/blocks_advanced.wren
vendored
@ -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
27
test/markdown/code_language.wren
vendored
Normal 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
27
test/markdown/tables.wren
vendored
Normal 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
29
test/markdown/task_lists.wren
vendored
Normal 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
72
test/regex/edge_cases.wren
vendored
Normal 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
6
test/regex/error_invalid_pattern.wren
vendored
Normal 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
48
test/regex/flags.wren
vendored
Normal 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
40
test/regex/match_all.wren
vendored
Normal 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
49
test/regex/match_class.wren
vendored
Normal 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
92
test/regex/patterns.wren
vendored
Normal 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
45
test/regex/replace.wren
vendored
Normal 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
63
test/regex/split.wren
vendored
Normal 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
36
test/regex/static_methods.wren
vendored
Normal 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
22
test/web/large_upload.wren
vendored
Normal 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
26
test/web/multipart.wren
vendored
Normal 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
21
test/web/multipart_binary.wren
vendored
Normal 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
15
test/web/multipart_lf_only.wren
vendored
Normal 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
24
test/web/multipart_text_only.wren
vendored
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user