fix: some small rework for doc alignment
This commit is contained in:
44
API.md
44
API.md
@@ -4,7 +4,7 @@ Contract version: 1.7.0
|
||||
|
||||
This reference documents all Python runtime API calls available through `bds_api` in embedded Pyodide.
|
||||
|
||||
`bds_api` is available in both **macro scripts** (executed during preview and page generation) and **transform scripts** (executed during blogmark import). In macro entrypoints, API calls run in the same runtime context as the macro and can be used to fetch posts, media, tags, or other application data.
|
||||
`bds_api` is available in **macro scripts** (executed during preview and page generation). Import `bds` and call API methods from an `async def` entrypoint. Transform scripts do not have access to `bds_api`.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -375,7 +375,7 @@ result = await bds.posts.create(data={})
|
||||
'slug': 'value',
|
||||
'excerpt': 'value',
|
||||
'content': 'value',
|
||||
'status': None,
|
||||
'status': 'draft',
|
||||
'author': 'value',
|
||||
'createdAt': 'value',
|
||||
'updatedAt': 'value',
|
||||
@@ -418,7 +418,7 @@ None # or
|
||||
'slug': 'value',
|
||||
'excerpt': 'value',
|
||||
'content': 'value',
|
||||
'status': None,
|
||||
'status': 'draft',
|
||||
'author': 'value',
|
||||
'createdAt': 'value',
|
||||
'updatedAt': 'value',
|
||||
@@ -485,7 +485,7 @@ None # or
|
||||
'slug': 'value',
|
||||
'excerpt': 'value',
|
||||
'content': 'value',
|
||||
'status': None,
|
||||
'status': 'draft',
|
||||
'author': 'value',
|
||||
'createdAt': 'value',
|
||||
'updatedAt': 'value',
|
||||
@@ -578,7 +578,7 @@ result = await bds.posts.get_by_status(status='status')
|
||||
'slug': 'value',
|
||||
'excerpt': 'value',
|
||||
'content': 'value',
|
||||
'status': None,
|
||||
'status': 'draft',
|
||||
'author': 'value',
|
||||
'createdAt': 'value',
|
||||
'updatedAt': 'value',
|
||||
@@ -621,7 +621,7 @@ None # or
|
||||
'slug': 'value',
|
||||
'excerpt': 'value',
|
||||
'content': 'value',
|
||||
'status': None,
|
||||
'status': 'draft',
|
||||
'author': 'value',
|
||||
'createdAt': 'value',
|
||||
'updatedAt': 'value',
|
||||
@@ -663,7 +663,7 @@ None # or
|
||||
'slug': 'value',
|
||||
'excerpt': 'value',
|
||||
'content': 'value',
|
||||
'status': None,
|
||||
'status': 'draft',
|
||||
'author': 'value',
|
||||
'createdAt': 'value',
|
||||
'updatedAt': 'value',
|
||||
@@ -804,7 +804,7 @@ result = await bds.posts.filter(filter={})
|
||||
'slug': 'value',
|
||||
'excerpt': 'value',
|
||||
'content': 'value',
|
||||
'status': None,
|
||||
'status': 'draft',
|
||||
'author': 'value',
|
||||
'createdAt': 'value',
|
||||
'updatedAt': 'value',
|
||||
@@ -996,7 +996,7 @@ result = await bds.posts.get_links_to(id='id-1')
|
||||
'slug': 'value',
|
||||
'excerpt': 'value',
|
||||
'content': 'value',
|
||||
'status': None,
|
||||
'status': 'draft',
|
||||
'author': 'value',
|
||||
'createdAt': 'value',
|
||||
'updatedAt': 'value',
|
||||
@@ -1038,7 +1038,7 @@ result = await bds.posts.get_linked_by(id='id-1')
|
||||
'slug': 'value',
|
||||
'excerpt': 'value',
|
||||
'content': 'value',
|
||||
'status': None,
|
||||
'status': 'draft',
|
||||
'author': 'value',
|
||||
'createdAt': 'value',
|
||||
'updatedAt': 'value',
|
||||
@@ -1736,7 +1736,7 @@ result = await bds.media.get_tags_with_counts()
|
||||
|
||||
### scripts.create
|
||||
|
||||
Create script.
|
||||
Create script. data must include: title (str), kind ("macro"|"utility"|"transform"), content (str). Optional: slug (str), entrypoint (str, defaults to "render"), enabled (bool).
|
||||
|
||||
**Parameters**
|
||||
|
||||
@@ -1762,7 +1762,7 @@ result = await bds.scripts.create(data={})
|
||||
'projectId': 'value',
|
||||
'slug': 'value',
|
||||
'title': 'value',
|
||||
'kind': None,
|
||||
'kind': 'macro',
|
||||
'entrypoint': 'value',
|
||||
'enabled': False,
|
||||
'version': 0,
|
||||
@@ -1775,7 +1775,7 @@ result = await bds.scripts.create(data={})
|
||||
|
||||
### scripts.update
|
||||
|
||||
Update script by id.
|
||||
Update script by id. data may include any of: title, kind, content, slug, entrypoint, enabled.
|
||||
|
||||
**Parameters**
|
||||
|
||||
@@ -1804,7 +1804,7 @@ None # or
|
||||
'projectId': 'value',
|
||||
'slug': 'value',
|
||||
'title': 'value',
|
||||
'kind': None,
|
||||
'kind': 'macro',
|
||||
'entrypoint': 'value',
|
||||
'enabled': False,
|
||||
'version': 0,
|
||||
@@ -1870,7 +1870,7 @@ None # or
|
||||
'projectId': 'value',
|
||||
'slug': 'value',
|
||||
'title': 'value',
|
||||
'kind': None,
|
||||
'kind': 'macro',
|
||||
'entrypoint': 'value',
|
||||
'enabled': False,
|
||||
'version': 0,
|
||||
@@ -1910,7 +1910,7 @@ result = await bds.scripts.get_all()
|
||||
'projectId': 'value',
|
||||
'slug': 'value',
|
||||
'title': 'value',
|
||||
'kind': None,
|
||||
'kind': 'macro',
|
||||
'entrypoint': 'value',
|
||||
'enabled': False,
|
||||
'version': 0,
|
||||
@@ -1985,7 +1985,7 @@ result = await bds.tasks.get_all()
|
||||
{
|
||||
'taskId': 'value',
|
||||
'name': 'value',
|
||||
'status': None,
|
||||
'status': 'pending',
|
||||
'progress': 0,
|
||||
'message': 'value',
|
||||
'startTime': 'value',
|
||||
@@ -2024,7 +2024,7 @@ result = await bds.tasks.get_running()
|
||||
{
|
||||
'taskId': 'value',
|
||||
'name': 'value',
|
||||
'status': None,
|
||||
'status': 'pending',
|
||||
'progress': 0,
|
||||
'message': 'value',
|
||||
'startTime': 'value',
|
||||
@@ -2635,7 +2635,7 @@ result = await bds.meta.sync_on_startup()
|
||||
'defaultAuthor': 'value',
|
||||
'maxPostsPerPage': 0,
|
||||
'blogmarkCategory': 'value',
|
||||
'pythonRuntimeMode': None,
|
||||
'pythonRuntimeMode': 'webworker',
|
||||
'picoTheme': 'value',
|
||||
'categoryMetadata': {},
|
||||
'categorySettings': {}
|
||||
@@ -2677,7 +2677,7 @@ None # or
|
||||
'defaultAuthor': 'value',
|
||||
'maxPostsPerPage': 0,
|
||||
'blogmarkCategory': 'value',
|
||||
'pythonRuntimeMode': None,
|
||||
'pythonRuntimeMode': 'webworker',
|
||||
'picoTheme': 'value',
|
||||
'categoryMetadata': {},
|
||||
'categorySettings': {}
|
||||
@@ -2718,7 +2718,7 @@ None # or
|
||||
'defaultAuthor': 'value',
|
||||
'maxPostsPerPage': 0,
|
||||
'blogmarkCategory': 'value',
|
||||
'pythonRuntimeMode': None,
|
||||
'pythonRuntimeMode': 'webworker',
|
||||
'picoTheme': 'value',
|
||||
'categoryMetadata': {},
|
||||
'categorySettings': {}
|
||||
@@ -2759,7 +2759,7 @@ None # or
|
||||
'defaultAuthor': 'value',
|
||||
'maxPostsPerPage': 0,
|
||||
'blogmarkCategory': 'value',
|
||||
'pythonRuntimeMode': None,
|
||||
'pythonRuntimeMode': 'webworker',
|
||||
'picoTheme': 'value',
|
||||
'categoryMetadata': {},
|
||||
'categorySettings': {}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
- [Working with pages](#working-with-pages)
|
||||
- [Working with media](#working-with-media)
|
||||
- [Using macros](#using-macros)
|
||||
- [Using scripting (early access)](#using-scripting-early-access)
|
||||
- [Using scripting](#using-scripting)
|
||||
- [Using the AI assistant](#using-the-ai-assistant)
|
||||
- [Organizing with tags](#organizing-with-tags)
|
||||
- [Importing from WordPress (WXR)](#importing-from-wordpress-wxr)
|
||||
@@ -194,21 +194,15 @@ The optional `orientation` parameter supports `horizontal`, `mixed_hv`, and `mix
|
||||
|
||||
---
|
||||
|
||||
## Using scripting (early access)
|
||||
## Using scripting
|
||||
|
||||
The scripting feature is an incremental capability and should currently be treated as early access. Scripts are stored as Python files in the project filesystem, while script metadata is tracked in the project database and embedded in the file metadata docstring block. This keeps scripts portable and inspectable while still allowing reliable indexing in the app.
|
||||
Scripts are Python files stored in your project's `scripts/` directory. Each file carries embedded YAML frontmatter in a docstring block at the top, which bDS uses to index the script in its database. This keeps scripts portable, Git-reviewable, and consistently tracked without a separate configuration file.
|
||||
|
||||
Each script exposes an **Entrypoint** selector. bDS always provides a synthetic `main` entrypoint. Selecting `main` runs the full script body as before. In addition, bDS inspects your script to list top-level Python function names, which can be selected as entrypoints for upcoming execution modes and integrations.
|
||||
Each script has a **Kind** (macro, transform, or utility) and an **Entrypoint** that names the Python function to invoke. bDS inspects your script to list all top-level function names so you can choose which one to call. Keep scripts versioned through your normal Git workflow, review changes carefully, and prefer small, focused scripts.
|
||||
|
||||
At this stage, scripting is intended for controlled project workflows where scripts interact with application-provided tools. Keep scripts versioned through your normal Git workflow, review changes carefully, and prefer small, explicit scripts over monolithic utility files.
|
||||
### Transform scripts
|
||||
|
||||
For transform scripts, bDS provides a built-in Python helper named `toast(message)`. It accepts a single string and emits a UI intent that the app handles on the renderer side. This keeps script ergonomics simple while preserving a controlled bridge between script runtime and user interface.
|
||||
|
||||
When transform scripts fail during a pipeline run, bDS automatically surfaces an error toast so users are notified immediately. Detailed transform diagnostics (applied scripts and per-script errors) are also written to the Output panel.
|
||||
|
||||
### Example transform script
|
||||
|
||||
Use a transform function to modify incoming bookmark/blogmark content before bDS creates the post. The function receives a mutable `post` dictionary and should return that dictionary.
|
||||
Transform scripts run during blogmark import to modify incoming content before bDS creates the post. The entrypoint function receives a mutable `post` dict and must return it.
|
||||
|
||||
```python
|
||||
def normalize_blogmark(post):
|
||||
@@ -237,10 +231,23 @@ def normalize_blogmark(post):
|
||||
return post
|
||||
```
|
||||
|
||||
You can also accept an optional `context` argument that bDS passes when importing a blogmark:
|
||||
|
||||
```python
|
||||
def normalize_blogmark(post, context=None):
|
||||
url = (context or {}).get("url", "")
|
||||
toast(f"Transform applied from: {url}")
|
||||
return post
|
||||
```
|
||||
|
||||
`context` contains `source` (always `"blogmark"`) and `url` (the original bookmarked URL).
|
||||
|
||||
Notes:
|
||||
- `title` and `content` are strings.
|
||||
- `categories` and `tags` are string lists (e.g., `['News', 'AI']`).
|
||||
- Return the mutated `post` dict from your transform function.
|
||||
- `toast(message)` is a built-in available in transform scripts to send user-facing notifications.
|
||||
- When a transform fails, bDS automatically surfaces an error toast and writes diagnostics to the Output panel.
|
||||
- Keep transforms small and deterministic, especially when multiple active transforms run in sequence.
|
||||
|
||||
### Macro scripts
|
||||
@@ -251,39 +258,47 @@ The entrypoint function always receives two arguments:
|
||||
|
||||
```python
|
||||
def render(context, post):
|
||||
params = context["params"] # dict of macro parameters
|
||||
language = context["language"] # project language code
|
||||
post_slug = context["post_slug"] # slug of the host post
|
||||
params = context.get("params") or {} # key-value pairs from [[slug key="value"]]
|
||||
env = context.get("env") or {}
|
||||
language = env.get("mainLanguage", "en") # project render language (generation only)
|
||||
is_preview = env.get("isPreview", False) # True when rendering in the editor preview
|
||||
|
||||
title = post["title"] if post else "Unknown"
|
||||
return {"html": f"<p>Post: {title}</p>"}
|
||||
title = (post or {}).get("title", "Unknown")
|
||||
custom_label = params.get("label", "")
|
||||
return {"html": f"<p>{title}: {custom_label} ({language})</p>"}
|
||||
```
|
||||
|
||||
`context` is a dict containing `params` (the key-value pairs from the macro tag), `language`, and `post_slug`. `post` is the full PostData dict for the post containing the macro, or `None` when post data is unavailable. The function must return a dict with an `html` key containing the rendered HTML string.
|
||||
`context` is a dict with two keys:
|
||||
- `params` — a dict of all key-value attributes from the macro tag. For example, `[[my_macro title="Hello"]]` gives `context["params"]["title"] == "Hello"`.
|
||||
- `env` — a dict containing `isPreview` (bool). During generation it also includes `mainLanguage` (the project's render language code) and `hook` (the macro slug as written in Markdown).
|
||||
|
||||
Macro scripts can also call the application API through the `bds_api` module:
|
||||
`post` is the full PostData dict for the post containing the macro, or `None` when post data is unavailable. The function must return a dict with an `html` key containing the rendered HTML string.
|
||||
|
||||
#### Using the application API in macros
|
||||
|
||||
Macro scripts can call the application API through the `bds_api` module. Because API calls are asynchronous, the entrypoint must be an `async def`:
|
||||
|
||||
```python
|
||||
from bds_api import bds
|
||||
|
||||
def render(context, post):
|
||||
async def render(context, post):
|
||||
tags = await bds.posts.get_tags()
|
||||
items = "".join(f"<li>{t}</li>" for t in tags)
|
||||
return {"html": f"<ul>{items}</ul>"}
|
||||
```
|
||||
|
||||
See [API.md](API.md) for the full reference of available `bds` module calls.
|
||||
|
||||
To use the macro in a post, write `[[your_slug param="value"]]` in Markdown. Built-in JS macros (youtube, vimeo, gallery, photo_archive, tag_cloud) always take priority over Python macros with the same slug.
|
||||
|
||||
### Key takeaways
|
||||
|
||||
- Scripting is available and intentionally evolving in small steps.
|
||||
- `main` is always available and preserves whole-script execution behavior.
|
||||
- Script files and metadata remain filesystem-friendly and Git-reviewable.
|
||||
- Transform scripts can call `toast("...")` to send user-facing UI notifications.
|
||||
- Transform scripts can directly manipulate `title`, `content`, `categories`, and `tags`.
|
||||
- Script files and metadata are filesystem-friendly and Git-reviewable.
|
||||
- Transform scripts mutate incoming blogmark `post` dicts before creation; `toast()` sends user notifications.
|
||||
- Transform scripts can accept an optional `context` arg with `source` and `url` from the blogmark.
|
||||
- Transform pipeline failures always trigger automatic error toasts.
|
||||
- Macro scripts use a two-argument entrypoint: `def render(context, post)`.
|
||||
- Macro scripts can call `bds_api` to access posts, media, tags, and other application data.
|
||||
- Macro entrypoints receive `(context, post)` — use `def render` for pure logic, `async def render` when calling `bds_api`.
|
||||
- `context["params"]` holds macro tag attributes; `context["env"]` holds runtime metadata including `isPreview` and `mainLanguage`.
|
||||
- Built-in JS macros always take priority over Python macros with the same slug.
|
||||
|
||||
[↑ Back to In this article](#in-this-article)
|
||||
|
||||
@@ -192,6 +192,7 @@ async function renderMacro(request: WorkerRenderMacroRequest): Promise<void> {
|
||||
|
||||
const rawResult = await runtime.runPythonAsync(`
|
||||
import json as _json
|
||||
import inspect as _inspect
|
||||
|
||||
_macro_ctx = _json.loads(__bds_macro_context_json)
|
||||
_macro_ep = __bds_macro_entrypoint
|
||||
@@ -200,7 +201,11 @@ if _macro_fn is None or not callable(_macro_fn):
|
||||
raise RuntimeError(f"Macro entrypoint '{_macro_ep}' is not callable")
|
||||
_macro_post_json = __bds_macro_post_data_json
|
||||
_macro_post = _json.loads(_macro_post_json) if _macro_post_json else None
|
||||
_macro_result = _macro_fn(_macro_ctx, _macro_post)
|
||||
_macro_call = _macro_fn(_macro_ctx, _macro_post)
|
||||
if _inspect.isawaitable(_macro_call):
|
||||
_macro_result = await _macro_call
|
||||
else:
|
||||
_macro_result = _macro_call
|
||||
if _macro_result is None:
|
||||
raise RuntimeError("Macro function returned None")
|
||||
if not isinstance(_macro_result, dict):
|
||||
|
||||
@@ -122,8 +122,8 @@ const METHODS_V1: PythonApiMethodContractV1[] = [
|
||||
method('media.getTags', 'Get all media tags.', [], 'string[]'),
|
||||
method('media.getTagsWithCounts', 'Get media tags with counts.', [], 'TagCount[]'),
|
||||
|
||||
method('scripts.create', 'Create script.', [requiredObject('data')], 'ScriptData'),
|
||||
method('scripts.update', 'Update script by id.', [requiredString('id'), requiredObject('data')], 'ScriptData | null'),
|
||||
method('scripts.create', 'Create script. data must include: title (str), kind ("macro"|"utility"|"transform"), content (str). Optional: slug (str), entrypoint (str, defaults to "render"), enabled (bool).', [requiredObject('data')], 'ScriptData'),
|
||||
method('scripts.update', 'Update script by id. data may include any of: title, kind, content, slug, entrypoint, enabled.', [requiredString('id'), requiredObject('data')], 'ScriptData | null'),
|
||||
method('scripts.delete', 'Delete script by id.', [requiredString('id')], 'boolean'),
|
||||
method('scripts.get', 'Fetch script by id.', [requiredString('id')], 'ScriptData | null'),
|
||||
method('scripts.getAll', 'Fetch all scripts.', [], 'ScriptData[]'),
|
||||
|
||||
@@ -125,6 +125,15 @@ function extractDataStructureNames(typeSignature: string): string[] {
|
||||
function sampleValueForField(type: string): string {
|
||||
const normalized = type.trim();
|
||||
|
||||
// Handle string literal union types like "'macro' | 'utility' | 'transform'"
|
||||
// These start with a single or double quote and contain literal enum values.
|
||||
if (/^['"]/.test(normalized)) {
|
||||
const firstLiteral = normalized.match(/['"]([^'"]+)['"]/);
|
||||
if (firstLiteral) {
|
||||
return `'${firstLiteral[1]}'`;
|
||||
}
|
||||
}
|
||||
|
||||
if (normalized.includes('string')) {
|
||||
return "'value'";
|
||||
}
|
||||
@@ -206,7 +215,7 @@ export function generateApiDocumentationMarkdownV1(): string {
|
||||
sections.push('');
|
||||
sections.push('This reference documents all Python runtime API calls available through `bds_api` in embedded Pyodide.');
|
||||
sections.push('');
|
||||
sections.push('`bds_api` is available in both **macro scripts** (executed during preview and page generation) and **transform scripts** (executed during blogmark import). In macro entrypoints, API calls run in the same runtime context as the macro and can be used to fetch posts, media, tags, or other application data.');
|
||||
sections.push('`bds_api` is available in **macro scripts** (executed during preview and page generation). Import `bds` and call API methods from an `async def` entrypoint. Transform scripts do not have access to `bds_api`.');
|
||||
sections.push('');
|
||||
sections.push('## Usage');
|
||||
sections.push('');
|
||||
|
||||
@@ -189,13 +189,19 @@ async function runMacroV1(request: PythonWorkerRequest): Promise<void> {
|
||||
|
||||
const rawJsonResult = await runtime.runPythonAsync(`
|
||||
import json as _json
|
||||
import inspect as _inspect
|
||||
_macro_ep = __bds_macro_entrypoint
|
||||
_macro_fn = globals().get(_macro_ep)
|
||||
if _macro_fn is None or not callable(_macro_fn):
|
||||
raise RuntimeError(f"Macro entrypoint '{_macro_ep}' is not callable")
|
||||
_macro_post_json = __bds_macro_post_data_json
|
||||
_macro_post = _json.loads(_macro_post_json) if _macro_post_json else None
|
||||
_json.dumps(_macro_fn(__bds_context_v1, _macro_post))
|
||||
_macro_call = _macro_fn(__bds_context_v1, _macro_post)
|
||||
if _inspect.isawaitable(_macro_call):
|
||||
_macro_result = await _macro_call
|
||||
else:
|
||||
_macro_result = _macro_call
|
||||
_json.dumps(_macro_result)
|
||||
`);
|
||||
|
||||
const parsedResult = parseMacroResultV1(JSON.parse(toResultString(rawJsonResult)));
|
||||
|
||||
@@ -196,10 +196,10 @@ describe('DocumentationView', () => {
|
||||
</I18nProvider>
|
||||
);
|
||||
|
||||
const targetHeading = await screen.findByRole('heading', { level: 2, name: 'Using scripting (early access)' });
|
||||
const targetHeading = await screen.findByRole('heading', { level: 2, name: 'Using scripting' });
|
||||
targetHeading.id = 'unexpected-id';
|
||||
|
||||
const tocLink = container.querySelector('a[href="#using-scripting-early-access"]') as HTMLAnchorElement;
|
||||
const tocLink = container.querySelector('a[href="#using-scripting"]') as HTMLAnchorElement;
|
||||
expect(tocLink).not.toBeNull();
|
||||
|
||||
const scrollContainer = container.querySelector('.documentation-scroll') as HTMLElement;
|
||||
@@ -221,7 +221,7 @@ describe('DocumentationView', () => {
|
||||
fireEvent.click(tocLink);
|
||||
|
||||
expect(scrollContainer.scrollTop).toBe(388);
|
||||
expect(targetHeading.id).toBe('using-scripting-early-access');
|
||||
expect(targetHeading.id).toBe('using-scripting');
|
||||
});
|
||||
|
||||
it('shows a copy button for fenced code blocks and copies the block content', async () => {
|
||||
|
||||
Reference in New Issue
Block a user