Data Sources
Using the same concept as a database you can read from these files:
Then optionally select nested objects with these query expressions:
Tip
These examples can also be used with Cloud and DB data sources except every provider has a different way to query their data.
Interface
import type { DataSource as IDataSource } from "../db/interface";
interface DataSource extends IDataSource {
source: "uri" | "local" | "export" | "json";
type: "text" | "attribute" | "display"; // Element action
viewEngine?: {
name: string; // NPM package name
singleRow?: boolean;
};
viewEngine?: string; // settings.view_engine[name]? + settings.users[username].view_engine[name]? (overlay)
/* OR */
value?: string | string[] | Record<string, unknown>; // Replace placeholders per row of results
/* OR */
template?: string; // Will replace "value" from local file
/* OR */
dynamic?: boolean; // Uses element.innerXml rather than element.textContent
query?: string; // startsWith("$") JSONPath otherwise JMESPath
ignoreCache?: boolean; // Bypass cache without saving
ignoreCache?: 1; // purge cache with saving
ignoreEmpty?: boolean; // Will not modify anything when there are no results
ignoreCoerce?: boolean; // Will not convert inline constructors to native objects
}
interface TextDataSource extends DataSource {
type: "text";
leadingText?: string;
trailingText?: string;
}
interface DataObject {
format: "json" | "yaml" | "json5" | "xml" | "toml";
options?: PlainObject; // Parser options (yaml + xml)
}
interface CascadeAction {
/*
When array [{ a: 1 }, { b: 2 }] then "0.a == 1" OR "1.b == 2" (leading only)
When multi-dimensional array { a: [[1], [2,3]] } then "a[0].0 == 1" OR "a.1.1 == 3" (brackets are optional)
When key has spaces { " a ": { "b ": { c: 1 } } } then " a "["b "].c OR " a "."b "[c] == 1 (quotes are optional)
When key has dot operator { a: { "b.c": 1 } } then a[b\\.c] == 1
Invalid usage a[0][1][2] instead use a.0.1.0 or a[0].1[2] (max is 1 consequetive bracket)
*/
cascade?: string; // Called before "query"
fallback?: object; // Used when there are missing fields
}
Inline “json”
interface JSONDataSource extends DataSource, CascadeAction {
source: "json";
items?: unknown;
}
Example usage
{
"selector": "main",
"type": "text",
"dataSource": {
"source": "json",
"items": [
{ "title": "1", "description": "first item" },
{ "title": "2", "description": "second item" }
],
"leadingText": "<ul>",
"value": "<li><b>${title}</b>: ${description}</li>", // <ul><li><b>1</b>: first item</li><li><b>2</b>: second item</li></ul>
"trailingText": "</ul>",
"items": { "title": "1", "description": "first item" },
"value": "<b>${title}</b>: ${description}", // <b>1</b>: first item
/* OR */
"items": {},
"value": "<b>1</b>: first item"
}
}
Remote file “uri”
interface UriDataSource extends DataSource, DataObject, CascadeAction {
source: "uri";
uri: string; // Will perform a fetch request
/* Method "POST" */
body?: unknown; // Required
contentType?: string;
}
Example usage
Reusing configuration templates is possible with URL search parameters. All parameters (excluding “value”) from any source can be replaced using the {{param}} syntax.
{
"selector": "main img",
"type": "attribute",
"dataSource": {
"source": "uri",
"format": "{{format}}", // json
"uri": "http://hostname/project/{{file}}.{{format}}", // http://hostname/project/demo.json
"query": "$[1]", // Row #2 in result array (JSONPath)
/* Result: { "src": "image.png", "other": { "alt": "description" } } */
"value": {
"src": "src",
"alt": "other.alt"
}
}
}
Caution
Query parameters will also replace values inside the attributes property.
{
"selector": "h1",
"type": "text",
"dataSource": {
"source": "uri",
"uri": "https://hostname/api/v1/items", // Perform secure API request
"body": {
"id": "1"
},
"format": "json", // Response headers["Accept"] (generated)
"contentType": "application/json", // Request headers["Content-Type"] (optional)
"value": "<b>${title}</b>: ${description}"
}
}
Note
JSON will be sent by default for POST when both format and contentType are empty.
Local file “local”
interface LocalDataSource extends DataSource, DataObject, CascadeAction {
source: "local";
pathname: string;
}
Example usage
{
"selector": "main img",
"type": "attribute",
"dataSource": {
"source": "local",
"format": "xml",
"pathname": "./path/data.xml", // yaml + json5 + toml + xml + cjs (settings.directory.data + users/username/?)
/* OR */
"pathname": "/path/to/data.xml", // Local files require read permissions
"query": "$.root.row[1]", // Second item in "row" array (JSONPath)
/* Result: { "title": "Tokyo", "description": "Japan" } */
"value": "<b>${__index__}. ${title}</b>: ${description}" // "__index__": Row index value
}
}
{
"selector": "main div",
"type": "display",
"dataSource": {
"source": "mongodb",
"uri": "mongodb://localhost:27017",
"removeEmpty": true, // Includes invalid conditions
"value": "attr1", // Remove when: null or undefined
"value": "-attr2", // Remove when: attr2=falsey
"value": "+attr3", // Remove when: attr3=truthy
/* OR */
"value": [
"attr1", // AND
":is(OR)",
"-attr2", "-attr3", // OR
":is(AND)",
"+attr4" // Remove when: attr1=null + attr2|attr3=falsey + attr4=truthy
]
}
}
To completely remove an element all AND conditions have to be true
and one OR per group is true
. Using a view engine is recommended if you require a more advanced statement.
Returning an empty result or a blank string (view engine) is false
.
External source “export”
Custom functions or packages can be used to return any kind of dataset from any source providing a temporary solution during development.
interface ExportDataSource {
source: "export";
params: unknown; // Passed into custom function (required)
pathname?: string; // Module file (.cjs) | Local file (.js) | inline function | NPM package
/* OR */
settings?: string;
/* OR */
execute?: (...args: unknown[]) => unknown;
persist?: boolean; // Default is "true"
}
Example settings
{
"document": {
"chrome": {
"handler": "@pi-r/chrome",
"eval": {
"function": true // Enable inline functions
},
"settings": {
"export": {
"data-example": "(params, resolve, require) => { const fs = require('fs'); resolve(JSON.parse(fs.readFileSync(params.uri))); }",
"async-example": "async (params, require) => { const fs = require('fs'); const result = await fs.promises.readFile(params.uri); return JSON.parse(result); }"
}
}
}
}
}
Example file “.cjs”
const pg = require("pg");
const config = {
host: "localhost",
user: "**********",
password: "**********",
database: "squared",
port: 5432,
ssl: true
};
module.exports = async function (params) {
const client = new pg.Client();
await client.connect();
const { rows } = await client.query("SELECT * FROM demo WHERE id = $1", [params.id]);
await client.end();
return rows;
};
Example file “.js”
function (params, resolve, require) { // async function (params, require)
const mysql = require("mysql");
const conn = new mysql.createConnection({
host: "localhost",
user: "**********",
password: "**********",
database: "squared",
port: 3306,
ssl: true
});
conn.connect();
conn.query("SELECT * FROM demo WHERE id = ?", [params.id], (err, result) => {
if (!err) {
resolve(result);
}
else {
console.log(err);
resolve(null);
}
});
conn.end();
}
Note
Using this approach with databases is not recommended.
Example usage
{
"selector": "main p",
"type": "text",
"dataSource": {
"source": "export",
"pathname": "npm:postgres-custom", // npm i postgres-custom
/* OR */
"pathname": "./path/to/postgres.cjs", // settings.directory.export + users/username/?
/* OR */
"settings": "data-example", // settings.export
"value": "`<b>${this.title}</b>: ${this.description} (${this.total * 2})`", // Function template literal (settings.eval.function)
/* golang template syntax - partial support */
"value": "{{if !expired}}<b>${title}</b>: ${description}{{else}}Expired{{end}}", // Non-nested single conditional truthy property checks
"value": "{{if not expired}}<b>${title}</b>: ${description}{{else}}Expired{{end}}", // Case sensitive
"value": "{{if and (user.total) (ge user.total postMin) (lt user.total postMax)}}<b>${title}</b>: ${description}{{else if (eq user.total 0)}}Expired{{end}}"
}
}
Important
Parenthesis where noted in value
are required.
View Engine
interface ViewEngine {
name: string; // NPM package name
singleRow?: boolean; // Template data is sent in one pass using an Array[]
outputEmpty?: boolean; // Pass empty results to template engine
options?: {
compile?: PlainObject; // template = engine.compile(value, options)
output?: PlainObject; // template({ ...options, ...result[index] })
};
}
Note
Template engines with a compile(string[, options]): (data?: Record<string, any>) => string
method are compatible.
Example usage
Using template
(external) is the same as value
(inline) except the reusable content is stored inside a cacheable template server directory.
{
"selector": "main",
"type": "text",
"dataSource": {
"source": "local",
"format": "json",
"pathname": "./path/to/data.json",
"viewEngine": "main-ejs", // settings.view_engine[viewEngine]
/* OR */
"viewEngine": {
"name": "ejs", // NPM package name
"singleRow": true
},
"value": "<b><%= title %></b>: <%= description %>",
/* OR */
"template": "./path/to/content.ejs", // settings.directory.template + users/username/?
/* OR */
"dynamic": true, // element.innerXml? (with tags)
"dynamic": false, // element.textContent? (plain text) || outerXml (parsed from source)
"encoding": "utf-8"
}
}
Event callbacks
You can create named callbacks for postQuery and preRender anywhere inside the HTML. It is more readable than inside a configuration file and can be reused for similiar queries.
interface DataSource {
postQuery?: string | ((result: unknown[], item: DataSource) => unknown[] | void);
preRender?: string | ((output: string, item: DataSource) => string | void);
whenEmpty?: string | ((result: unknown[], item: DataSource) => unknown[] | void);
}
Example usage
Only one function can be defined per <script type="text/template">
element.
<script type="text/template" data-chrome-template="data::postQuery-example">
async function (result /* PlainObject[] */, dataSource) {
if (result.length) {
return await fetch("/db/url", { method: "POST", body: JSON.stringify(result) }).then(data => data.map(item => ({ name: item.key, value: item.value })));
}
return null; // "result" will display unmodified when not an array
}
</script>
<script type="text/template" data-chrome-template="data::preRender-example">
function (value /* string */, dataSource) {
return value.replaceAll("<", "<");
}
</script>
<script type="text/template" data-chrome-template="data::whenEmpty-example">
function (result /* PlainObject[] */, dataSource) {
result[0] = { value: "Empty" }; // result.length is 0
}
</script>
{
"selector": "",
"type": "data",
"template": {
"identifier": "postQuery-example",
"value": "async function (result, dataSource) {/* Same */}" // Arrow functions not supported
}
}
Warning
Using <script>
templates requires the setting eval.template = true
.
@pi-r/chrome
Added in version 0.7.0:
DataSource property ignoreCoerce was created.
Added in version 0.6.4:
UriDataSource property contentType | body for HTTP method POST was implemented.
DataSource property source option “json” as JSONDataSource was implemented.
TextDataSource property leadingText | trailingText were created.