diff --git a/API.md b/API.md index 2a08d67..027455f 100644 --- a/API.md +++ b/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': {} diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 32633ed..cdc3080 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -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,53 +194,60 @@ 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): - # 1) Manipulate title - title = (post.get("title") or "").strip() - if title and not title.startswith("[Clipped]"): - post["title"] = f"[Clipped] {title}" + # 1) Manipulate title + title = (post.get("title") or "").strip() + if title and not title.startswith("[Clipped]"): + post["title"] = f"[Clipped] {title}" - # 2) Manipulate text/content - content = (post.get("content") or "").strip() - prefix = "Imported from blogmark\n\n" - if content and not content.startswith(prefix): - post["content"] = prefix + content + # 2) Manipulate text/content + content = (post.get("content") or "").strip() + prefix = "Imported from blogmark\n\n" + if content and not content.startswith(prefix): + post["content"] = prefix + content - # 3) Set or replace categories - post["categories"] = ["Inbox", "Research"] + # 3) Set or replace categories + post["categories"] = ["Inbox", "Research"] - # 4) Add and normalize tags - tags = post.get("tags") or [] - tags.append("blogmark") - tags.append("clipped") - post["tags"] = sorted({str(tag).strip().lower() for tag in tags if str(tag).strip()}) + # 4) Add and normalize tags + tags = post.get("tags") or [] + tags.append("blogmark") + tags.append("clipped") + post["tags"] = sorted({str(tag).strip().lower() for tag in tags if str(tag).strip()}) - # 5) Optional user notification - toast(f"Transform applied: {post.get('title')}") - return post + # 5) Optional user notification + toast(f"Transform applied: {post.get('title')}") + 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"
Post: {title}
"} + title = (post or {}).get("title", "Unknown") + custom_label = params.get("label", "") + return {"html": f"{title}: {custom_label} ({language})
"} ``` -`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"