squared 5.4
Documentation
HTML
[!NOTE]
Content in the README was migrated into Read the Docs
. The file is no longer fully maintained.
README
Installation
NPX
> npm init
> npm i sqd-cli sqd-serve
> npx sqd init
> npx sqd init --public --local-serve
> npx sqd serve --access-all
> node serve.cjs --access-all
> npm init
> npm i squared sqd-serve
> mkdir dist html
> cp -r ./node_modules/squared/dist/* ./dist
> cp ./node_modules/squared/html/* ./html
> cp ./node_modules/sqd-serve/config/json/* .
> npx serve
GitHub
> git clone https://github.com/anpham6/squared
> cd squared
> npm i
> npm run prod
> cd ..
> git clone https://github.com/anpham6/squared-express
> cd squared-express
> npm i
> npm run prod
> npm run deploy
> cd ../squared
> node serve.cjs --access-all
Repo
Install
mkdir -p ~/bin/repo
PATH="${HOME}/bin:${PATH}"
curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
chmod a+rx ~/bin/repo
scripts/repo-install.sh ~/bin
Usage
mkdir workspaces
cd workspaces
repo init -u https://github.com/anpham6/squared-repo -m latest.xml
repo sync -j4
cd squared
npm i
Ruby
Workspace management uses Ruby for syncing and building. It is not installed by default on most operating systems.
mkdir workspaces
cd workspaces
wget https://raw.githubusercontent.com/anpham6/squared/master/Rakefile
rake -T
rake repo:init
rake repo:init[latest]
rake repo:init[0.11.x]
REPO_ROOT=/tmp/123 NODE_INSTALL=pnpm repo:init
[!TIP]
Use the supplied Rakefile inside the squared project folder once the source has been downloaded.
Docker
docker build -t squared --build-arg MANIFEST=prod --build-arg SQUARED=prod .
docker build -t node --build-arg NODE_TAG=20 --build-arg NODE_INSTALL=pnpm -f Dockerfile.slim .
docker build -t ruby --build-arg RUBY_TAG=3.0 --build-arg NODE_VERSION=20.x --build-arg PIPE_FAIL=0 -f Dockerfile.ruby .
docker run -it --name express --rm -p 127.0.0.1:80:80 \
--mount type=bind,src=${PWD},dst=/workspaces/squared/.config \
--mount type=bind,src=${PWD}/html,dst=/workspaces/squared/www \
squared
docker run -it --name debian squared /bin/bash
Browser
Usage
Library files are in the /dist
folder. A minimum of two files are required to run squared.
- squared
- squared-base - required: except vdom-lite
- squared-svg - optional
- framework (e.g. android | chrome | vdom | vdom-lite)
- extensions - optional
Usable combinations: 1-2-4 + 1-2-4-5 + 1-2-3-4-5 + 1-vdom-lite
File bundles for common combinations are available in the /dist/bundles
folder and do not require a call to setFramework.
[!WARNING]
Libraries in bold are transpiled with ES2020.
Example: android
The primary function parseDocument
can be called on multiple elements and multiple times per session. The application will continuously and progressively build the layout files into a single entity with combined shared resources.
[!CAUTION]
Using parseDocumentSync
is not recommended when your page has images or fonts.
<script src="/dist/squared.min.js"></script>
<script src="/dist/squared.base.min.js"></script>
<script src="/dist/squared.svg.min.js"></script>
<script src="/dist/android.framework.min.js"></script>
<script>
squared.settings.targetAPI = 35;
document.addEventListener("DOMContentLoaded", async () => {
squared.setFramework(android, {});
await squared.parseDocument(): Node
await squared.parseDocument(, , ): Node[]
await squared.parseDocument(
{
element: document.body,
projectId: "project-1",
resourceQualifier: "land",
resourceQualifier: {
suffix: "land",
layout: true,
string: undefined,
font: false,
image: "hdpi",
video: "w720dp",
audio: "w720dp",
animation: "v34",
menu: ""
},
enabledMultiline: false,
enabledSubstitute: true,
include: ["android.substitute"],
exclude: ["squared.list", "squared.grid"],
excludeQuery: [{
selector: "main > article",
resource: squared.base.lib.constant.NODE_RESOURCE.BOX_STYLE,
procedure: squared.base.lib.constant.NODE_PROCEDURE.OPTIMIZATION,
section: squared.base.lib.constant.APP_SECTION.DOM_TRAVERSE
}],
customizationsBaseAPI: -1,
observe(mutations, observer, settings) {
squared.reset();
squared.parseDocument(settings).then(() => {
squared.copyTo("/path/project", { modified: true }).then(response => console.log(response));
});
},
afterCascade(sessionId, node) {},
beforeRender(layout: LayoutUI) {},
afterFinalize(node: NodeUI) {}
},
{
element: "fragment-1",
projectId: "project-1",
resourceQualifier: "land",
pathname: "app/src/main/res/layout-hdpi",
filename: "fragment.xml",
baseLayoutAsFragment: {
name: "androidx.navigation.fragment.NavHostFragment",
documentId: "main_content",
app: {
navGraph: "@navigation/product_list_graph",
defaultNavHost: "true"
}
},
beforeCascade(sessionId) {
document.getElementById("fragment-id").style.display = "block";
}
}
);
await squared.parseDocument({
element: "fragment-2",
projectId: "sqd2",
resourceQualifier: "port",
enabledFragment: true,
fragmentableElements: [
{
selector: "main",
name: "androidx.navigation.fragment.NavHostFragment",
filename: "navigation.xml",
documentId: "main_content"
},
"main > article"
],
options: {
"android.resource.fragment": {
dynamicNestedFragments: true
}
}
});
squared.prefetch("css").then(() => squared.parseDocument());
Promise.all(
squared.prefetch("css", true),
squared.prefetch("css", "./undetected.css", element.shadowRoot),
squared.prefetch("svg", "http://embedded.example.com/icon.svg", "../images/android.svg")
)
.then(() => squared.parseDocument());
const body = squared.findDocumentNode(document.body);
body.android("layout_width", "match_parent");
body.lockAttr("android", "layout_width");
await squared.close();
squared.kill("30s").then(result => {});
await squared.save(, );
await squared.saveAs(, { projectId: "project-1" });
await squared.saveAs(, { timeout: 10 });
await squared.saveAs(, { throwErrors: true }).catch(err => console.log(err));
await squared.copyTo(, {});
await squared.copyTo(, { modified: true });
await squared.appendTo(, {});
squared.observe();
await squared.observeSrc(
"link[rel=stylesheet]",
(ev, element) => {
squared.reset();
squared.parseDocument().then(() => squared.copyTo("/path/project"));
},
{ port: 8080, secure: false, action: "reload" , expires: "1h" }
);
squared.reset();
});
</script>
[!CAUTION]
Calling saveAs
or copyTo
methods before the images have completely loaded can cause them to be excluded from the generated layout. In these cases you should use the asynchronous parseDocument
method to set a callback for your commands.
Example: chrome
Used primarly for developing single page layouts but can also bundle assets using query selector syntax. It is adequate for most projects and gives you the ability to develop your application as a module in place.
<script src="/dist/squared.min.js"></script>
<script src="/dist/squared.base.min.js"></script>
<script src="/dist/chrome.framework.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", async () => {
squared.setFramework(chrome, {});
await squared.save();
await squared.saveAs(, {});
await squared.copyTo(, {});
await squared.appendTo(, {});
await squared.copyTo(, { useOriginalHtmlPage: false, observe: | true }).then(() => squared.observe());
});
</script>
Example: vdom
The most minimal framework possible (55kb gzipped) and can be useful when debugging through DevTools. The lite
version is about half the bundle size and is recommended for most browser applications.
<script src="/dist/squared.min.js"></script>
<script src="/dist/squared.base-dom.min.js"></script>
<script src="/dist/vdom.framework.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", async () => {
squared.setFramework(vdom, {});
const element = squared.querySelector("body", true );
const elements = await squared.querySelectorAll("*");
const element = squared.fromElement(document.body, true );
const elements = await squared.getElementById("content-id").querySelectorAll("*");
});
</script>
There are ES2018 minified versions (*.min.js) and also ES2018 non-minified versions.
User Settings
These settings are available in the global variable squared
to customize your desired output structure. Each framework shares a common set of settings and also a subset of their own settings.
Example: android
squared.settings = {
targetAPI: 35,
supportRTL: true,
supportNegativeLeftTop: true,
preloadImages: true,
preloadFonts: true,
preloadLocalFonts: true,
preloadCustomElements: true,
enabledSVG: true,
enabledMultiline: true,
enabledViewModel: true,
enabledIncludes: false,
enabledFragment: false,
enabledSubstitute: false,
enabledCompose: false,
dataBindableElements: [],
includableElements: [],
substitutableElements: [],
fragmentableElements: [],
composableElements: [],
baseLayoutAsFragment: false | "fragment-name" | ["fragment-name", "fragment-tag"] | { selector, pathname?, filename?, name?, tag? },
baseLayoutToolsIgnore: "",
fontMeasureAdjust: 0.75,
lineHeightAdjust: 1.1,
preferMaterialDesign: false | "MaterialComponents" | "Material3",
createDownloadableFonts: true,
createElementMap: false,
pierceShadowRoot: true,
adaptStyleMap: true,
lockElementSettings: true,
customizationsBaseAPI: 0,
customizationsBaseAPI: [0, 33, 34],
removeDeprecatedAttributes: true,
removeDeprecatedAttributes: ["enabled", "singleLine"],
removeUnusedResourceViewId: false,
idNamingStyle: "android",
idNamingStyle: "html",
idNamingStyle: {
"__default__": "html",
"DIV": "comments",
"svg": ["vector", 0],
"#text": "text",
"::first-letter": "dropcap",
"main > section": ["content", 1, 2],
"form input[type=submit]": function(node) {
return "submit_" + node.id;
}
},
customizationsOverwritePrivilege: true,
outputMainFileName: "activity_main.xml",
outputFragmentFileName: "fragment_main.xml",
resourceQualifier: "",
resourceSystemColors: {
"system_accent1_100": "white",
"system_accent1_200": ['#ff0000', 0.75],
"system_accent1_300": squared.lib.color.parseColor("#000", 1)
},
manifestPackage: "",
manifestLabelAppName: "android",
manifestThemeName: "AppTheme",
manifestParentThemeName: "Theme.AppCompat.Light.NoActionBar",
manifestActivityName: ".MainActivity",
outputDocumentEditing: true,
outputDocumentCSS: [],
outputDirectory: "app/src/main",
createManifest: false,
createBuildDependencies: false | "ktx" | "baseline-profile" | ["ktx", "baseline-profile"],
builtInExtensions: [
"squared.accessibility",
"android.delegate.background",
"android.delegate.negative-x",
"android.delegate.positive-x",
"android.delegate.max-width-height",
"android.delegate.percent",
"android.delegate.content",
"android.delegate.scrollbar",
"android.delegate.radiogroup",
"android.delegate.multiline",
"squared.relative",
"squared.css-grid",
"squared.flexbox",
"squared.table",
"squared.column",
"squared.list",
"squared.grid",
"squared.sprite",
"squared.whitespace",
"android.resource.background",
"android.resource.svg",
"android.resource.strings",
"android.resource.fonts",
"android.resource.dimens",
"android.resource.styles",
"android.resource.data",
"android.resource.includes",
"android.substitute",
"android.resource.fragment",
"jetpack.compose.view"
],
compressImages: false,
compressImages: "****************",
convertImages: "",
showAttributes: true,
showAttributes: {
"hyphenationFrequency": "full",
"android:fontFeatureSettings": null,
"app:menu": [
"@menu/menu_1", "@menu/menu_2",
"@menu/menu_3", null
],
"app:menu": {
"@menu/menu_1": "@menu/menu_2",
"@menu/menu_3": null
}
},
showComments: false | ["boxShadow"] | { self: ["boxShadow"], nextSibling: ["marginBottom"], previousSibling: ["marginTop"], parent: ["position", "top", "left"] },
showComments: { include: { tagName: true | ["button"], attributes: true | ["style"], dataset: false, bounds: true }, self: ["boxShadow", ".className"] },
showErrorMessages: false,
convertPixels: "dp",
convertLineHeight: "sp",
convertEntities: ["numeric"],
convertEntities: ["codepoints", {}],
insertSpaces: 4,
outputDocumentHandler: "android",
outputEmptyCopyDirectory: false,
outputSummaryModal: false | "path/summary.css" | ".status-4 { color: purple; }",
outputTasks: {
"**/drawable/*.xml": { handler: "gulp", task: "minify" }
},
outputWatch: {
"**/drawable/*.png": true,
"**/drawable/*.jpg": { interval: 1000, expires: "2h" }
},
outputArchiveName: "android-xml",
outputArchiveFormat: "zip",
outputArchiveCache: false
};
squared.settings = {
resolutionDPI: 160,
resolutionScreenWidth: 1280,
resolutionScreenHeight: 800,
framesPerSecond: 60,
useShapeGeometryBox: true,
formatUUID: "8-4-4-4-12",
formatDictionary: "0123456789abcdef",
outputConfigName: "sqd.config",
observePort: 8080,
observeSecurePort: 8443,
observeExpires: "1h",
broadcastPort: 3080,
broadcastSecurePort: 3443
};
Example: chrome
squared.settings = {
preloadImages: false,
preloadFonts: false,
preloadLocalFonts: false,
preloadCustomElements: false,
excludePlainText: true,
createElementMap: true,
pierceShadowRoot: true,
adaptStyleMap: false,
builtInExtensions: [],
showErrorMessages: false,
webSocketPort: 80,
webSocketSecurePort: 443,
outputDocumentHandler: "chrome",
outputEmptyCopyDirectory: false,
outputSummaryModal: false,
outputTasks: {
"*.js": [{ handler: "gulp", task: "minify" }, { handler: "gulp", task: "beautify" }]
},
outputWatch: { "*": true },
outputArchiveName: "chrome-data",
outputArchiveFormat: "zip",
outputArchiveCache: false
};
Example: vdom
squared.settings = {
createElementMap: true,
pierceShadowRoot: false,
adaptStyleMap: false,
builtInExtensions: [],
showErrorMessages: false
};
Local Storage
Custom named user settings per framework can be saved to local storage as JSON and reused across all pages in the same domain. Extensions are configured using the same procedure.
squared.setFramework(android, { compressImages: true }, "android-example");
squared.setFramework(android, "android-example");
await squared.copyTo("/path/project", {}, "copy-example", true);
await squared.copyTo("/path/project", {}, "http://localhost:3000/copy-to/base-config.json");
await squared.copyTo("/path/project", {}, "copy-example");
await squared.copyTo("/path/project", "http://localhost:3000/copy-to/base-config.json");
await squared.copyTo("/path/project", "copy-example");
Public Properties and Methods
.settings
setFramework(app: {}, options?: PlainObject, setting?: string, cache?: boolean)
setFramework(app: {}, loadName: string, cache?: boolean)
setHostname(value: string )
setEndpoint(name: string, value: string)
setLocalAddress(...values: (string | URL | Location)[])
prefetch(type: "css" | "javascript" | "image" | "svg", all?: boolean, ...targets: unknown[])
parseDocument(...elements: (HTMLElement | string | ElementSettings)[])
parseDocumentSync(...elements: (HTMLElement | string | ElementSettings)[])
latest(count?: number)
auth(token: string)
save(projectId?: string)
save(projectId?: string, broadcastId?: string)
save(projectId?: string, timeout?: number)
close(projectId?: string)
reset(projectId?: string)
clear()
toString()
toString(projectId: string)
add(...names: (string | Extension | ExtensionRequestObject)[])
remove(...names: (string | Extension)[])
get(...names: string[])
attr(name: string | Extension, attrName: string, value?: unknown)
apply(name: string | Extension, options: PlainObject, setting?: string)
extend(functionMap: {}, framework?: )
observe(value?: boolean | MutationObserverInit)
broadcast(callback: BroadcastMessageCallback, options: FileBroadcastOptions | string)
getElementById(value: string, sync?: boolean, cache?: boolean)
querySelector(value: string, sync?: boolean, cache?: boolean)
querySelectorAll(value: string, sync?: boolean, cache?: boolean)
fromElement(element: HTMLElement | string, sync?: boolean, cache?: boolean)
fromNode(node: Node, sync?: boolean, cache?: boolean)
findDocumentNode(element: HTMLElement | string , all?: boolean)
observeSrc(element: HTMLElement | string , callback: WebSocketMessageChange, options?: FileObserveOptions)
observeSrc(element: HTMLElement | string, options: FileObserveOptions)
Packaging methods will return a Promise and requires a squared-express installation. These features are not supported when the framework is VDOM.
saveAs(filename: string, options?: {}, setting?: string, overwrite?: boolean)
saveFiles(filename: string, options: {}, setting?: string, overwrite?: boolean)
appendTo(pathname: string, options?: {}, setting?: string, overwrite?: boolean)
appendFiles(pathname: string, options: {}, setting?: string, overwrite?: boolean)
copyTo(pathname: string | string[], options?: {}, setting?: string, overwrite?: boolean)
copyFiles(pathname: string | string[], options: {}, setting?: string, overwrite?: boolean)
kill(pid: number, timeout?: number)
kill(timeout: string)
kill()
kill(0)
kill(-1, 10)
kill(NaN, 10)
kill("10s")
Extending Node object
You can add functions and initial variables to the Node object including overwriting preexisting class definitions per framework. Accessor properties are also supported using the get/set object syntax.
squared.extend({
_id: 1,
altId: {
get() {
return this._id;
},
set(value) {
this._id += value;
}
},
customId: {
value: 2,
configurable: false,
enumerable: false
},
addEvent(eventName, callback) {
this.element.addEventListener(eventName, callback);
}
});
squared.setFramework(vdom);
const body = await squared.fromElement(document.body);
body.altId = 2;
body.addEvent("click", function (ev) {
this.classList.toggle("example");
});
Forwarding Request
Using another identical remote server to build the project when performing a saveAs
or copyTo
request can be achieved by changing only the origin address.
squared.setHostname("http://hostname:8000");
squared.setHostname();
await squared.saveAs("chrome.zip");
await squared.copyTo("/path/project");
Broadcasting
Console messages (stdout) can be sent to the browser console instead through DevTools.
squared.broadcast(result => { console.log(result.value); }, "111-111-111");
squared.broadcast(result => { console.log(result.value); }, "222-222-222");
squared.broadcast(result => { console.log(result.value); }, { socketId: "333-333-333", socketKey: "socket_id" });
await squared.copyTo("/path/project/project-1", {
projectId: "project-1",
log: { useColor: true },
broadcastId: "222-222-222"
});
Extension Configuration
Layout rendering can be customized using extensions as the program was built to be nearly completely modular. Some of the common layouts already have built-in extensions which you can load or unload based on your preference.
class Sample extends squared.base.Extension {
options = {
attributeName: [];
};
constructor(name, framework = 0, options = {}) {
super(name, framework, options);
}
processNode(node: NodeUI) {
const data = this.project.get(node.element, node.localSettings.projectId);
if (data) {
node.each((child, index) => child.element.title = data[index]);
}
}
}
const sample = new Sample("widget.example.com", 0, {});
squared.add(sample);
squared.add([sample, {}]);
squared.attr("widget.example.com", "attributeName", ["width", "height"]);
const ext = squared.get("widget.example.com");
ext.project.set(element, await fetch(url?id=1));
ext.project.set(element, await fetch(url?id=2), "project-1");
const data = ext.project.get(element, "project-2");
Some extensions have a few settings which can be configured. The default settings usually achieve the best overall rendering accuracy without noticeably affecting performance.
ANDROID
Public Methods
android.setViewModel(data: {}, sessionId?: string)
android.setViewModelByProject(data: {}, projectId?: string)
android.removeObserver(element: HTMLElement)
android.addXmlNs(name: string, uri: string)
android.addDependency(group: string, name: string, version?: string, type?: number)
android.addDependencyByProject(projectId: string, group: string, name: string, version?: string, type?: number)
android.customize(build: number, tagNameOrWidget: string, options: {})
android.loadCustomizations(name: string)
android.saveCustomizations(name: string)
android.resetCustomizations()
android.addFontProvider(authority: string, package: string, certs: string[], webFonts: string | {})
android.setResolutionByDeviceName(value: string)
android.getLocalSettings()
android.customize(android.lib.constant.BUILD_VERSION.ALL , "Button", {
android: {
minWidth: "35px",
minHeight: "25px"
},
"_": {
style: "@style/Widget.Material3.Button.TextButton"
}
});
android.customize(android.lib.constant.BUILD_VERSION.KITKAT , "svg", {
android: {
"[src]": "app:srcCompat"
}
});
android.saveCustomizations("customize-example");
android.loadCustomizations("customize-example");
android.addXmlNs("tools", "http://schemas.android.com/tools");
android.customize(16 , "ImageView", {
tools: {
ignore: "ContentDescription",
targetApi: "16"
}
});
Static Methods
Project resources can include additional values that are required during compilation. TypeScript definitions are available in the types/android
directory.
squared.parseDocument().then(node => {
const resourceId = node.localSettings.resourceId;
android.base.Resource.addString(resourceId, value, );
android.base.Resource.addArray(resourceId, name, items);
android.base.Resource.addColor(resourceId, color);
android.base.Resource.addDimen(resourceId, name, value);
android.base.Resource.addTheme(resourceId, theme);
squared.save();
});
Data Binding
View model data can be applied to most HTML elements using the dataset attribute. Different view models can be used for every parseDocument
session.
Leaving the sessionId
empty uses the default view model which is searched last for all projects when attempting a bind.
squared.parseDocument("id-1", "id-2", "id-3").then(nodes => {
const sessions = squared.latest(2);
android.setViewModel(
{
import: ["java.util.Map", "java.util.List"],
variable: [
{ name: "user", type: "com.example.User" },
{ name: "list", type: "List<String>" },
{ name: "map", type: "Map<String, String>" },
{ name: "index", type: "int" },
{ name: "key", type: "String" }
]
},
sessions[0]
);
android.setViewModel(
{
import: ["java.util.Map"],
variable: [
{ name: "map", type: "Map<String, String>" }
]
},
sessions[1]
);
});
squared.parseDocument({
element: "main",
enabledViewModel: true,
dataBindableElements: [
{
selector: "#first_name",
namespace: "android",
attr: "text",
expression: "user.firstName"
},
{
selector: "#last_name",
attr: "text",
expression: "user.lastName"
},
{
selector: "#remember_me",
attr: "checked",
expression: "user.rememberMe",
twoWay: true
}
],
data: {
viewModel: {
import: ["java.util.Map"],
variable: [
{ name: "map", type: "Map<String, String>" }
]
}
}
});
squared.save();
Inlining is also supported and might be more convenient for simple layouts. JavaScript is recommended when you are calling parseDocument
multiple times.
data-viewmodel-{namespace}-{attribute} -> data-viewmodel-android-text
These two additional output parameters are required when using the "data-viewmodel" prefix.
<div id="main">
<label>Name:</label>
<input id="first_name" type="text" data-viewmodel-android-text="user.firstName" />
<input id="last_name" type="text" data-viewmodel-android-text="user.lastName" />
<input id="remember_me" type="checkbox" data-viewmodel-android-checked="=user.rememberMe" />
</div>
<layout>
<data>
<import type="java.util.Map" />
<import type="java.util.List" />
<variable name="user" type="com.example.User" />
<variable name="list" type="List<String>" />
<variable name="map" type="Map<String, String>" />
<variable name="index" type="int" />
<variable name="key" type="String" />
</data>
<LinearLayout android:id="@+id/main">
<TextView android:text="Name:" />
<EditText
android:id="@+id/first_name"
android:inputType="text"
android:text="@{user.firstName}" />
<EditText
android:id="@+id/last_name"
android:inputType="text"
android:text="@{user.lastName}" />
<CheckBox
android:id="@+id/remember_me"
android:checked="@={user.rememberMe}" />
</LinearLayout>
</layout>
Layout Includes / Merge Tag
Some applications can benefit from using includes or merge tags to share common templates. Nested includes is supported.
<div>
<div id="item1">Item 1</div>
<div id="item2" data-android-include-start="true" data-android-include-merge="true" data-pathname-android="app/src/main/res/layout-land" data-filename-android="filename1.xml">Item 2</div>
<div id="item3">Item 3</div>
<div id="item4" data-android-include-end="true">Item 4</div>
<div id="item5" data-android-include="filename2" data-android-include-end="true" data-android-include-viewmodel="exampleData">Item 5</div>
</div>
android.setViewModelByProject({ variable: [{ name: "exampleData", type: "com.example.ExampleData" }] }, "project-1");
squared.parseDocument({
element: document.body,
projectId: "project-1",
enabledIncludes: true,
includableElements: [
{
selectorStart: "#item2",
selectorEnd: "#item4",
pathname: "app/src/main/res/layout-land",
filename: "filename1.xml",
merge: true
},
{
selectorStart: "#item5",
selectorEnd: "#item5",
filename: "filename2",
viewModel: "exampleData"
}
]
});
[!NOTE]
By sessionId has precedence when associating a view model.
<LinearLayout>
<TextView>Item 1</TextView>
<include layout="@layout/filename1" />
<include layout="@layout/filename2" app:exampleData="@{exampleData}" />
</LinearLayout>
<merge>
<TextView>Item 2</TextView>
<TextView>Item 3</TextView>
<TextView>Item 4</TextView>
</merge>
<layout>
<data>
<variable name="exampleData" type="com.example.ExampleData" />
</data>
<TextView>Item 5</TextView>
</layout>
The attributes "data-android-include-start" and "data-android-include-end" can only be applied to elements which share the same parent container. See /demos/gradient.html
for usage instructions.
[!TIP]
"data-pathname-android" AND "data-filename-android" can also be used with any parseDocument
base element.
Redirecting Output Location
Sometimes it is necessary to extract elements and append them into other containers for it to look identical on the Android device. Redirection will fail if the target location is not a block/container element.
<div>
<span>Item 1</span>
<span data-android-target="location">Item 2</span>
<span data-android-target="location" data-android-target-index="1">Item 3</span>
<div>
<ul id="location">
<li>Item 4</li>
<li>Item 5</li>
</ul>
<LinearLayout>
<TextView>Item 1</TextView>
</LinearLayout>
<LinearLayout>
<TextView>Item 4</TextView>
<TextView>Item 3</TextView>
<TextView>Item 5</TextView>
<TextView>Item 2</TextView>
</LinearLayout>
Using target
into a ConstraintLayout or RelativeLayout container will not include automatic positioning.
Custom Attributes
System or extension generated attributes can be overridden preceding finalization. They will only be visible on the declared framework.
data-android-attr-{namespace}? -> Default is "android"
<div id="customId"
data-android-attr="layout_width::match_parent;layout_height::match_parent"
data-android-attr-app="layout_scrollFlags::scroll|exitUntilCollapsed">
</div>
<LinearLayout
android:id="@+id/customId"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed" />
const node = squared.findDocumentNode("customId");
node.android("layout_width", "match_parent");
node.android("layout_height", "match_parent");
node.app("layout_scrollFlags", "scroll|exitUntilCollapsed");
SVG animations
Only the XML based layout and resource files can be viewed on the Android device/emulator without any Java/Kotlin backend code. To play animations in the emulator you also have to start
the animation in MainActivity.java.
import android.graphics.drawable.Animatable;
android.widget.ImageView imageView1 = findViewById(R.id.imageview_1);
if (imageView1 != null) {
Animatable animatable = (Animatable) imageView1.getDrawable();
animatable.start();
}
Jetpack Compose
Most mobile applications do not have a deeply nested hierarchy and are generally better to implement using declarative programming.
squared.settings.composableElements = ["main", "#content", "--boxShadow", "--height=300px", {
selector: "main",
android: {
layout_height: "match_parent"
},
tools: {
composableName: "com.example.compose.Preview"
}
}];
squared.settings.createBuildDependencies = true;
You can also do it using the "android.substitute" extension directly inside the HTML element.
squared.add(["android.substitute", {
element: {
content: { android: { layout_width: "match_parent" } }
}
}]);
const items = squared.attr("android.substitute", "viewAttributes");
items.push("hint", "buttonTint");
squared.attr("android.substitute", "viewAttributes", items.concat(["hint", "buttonTint"]));
squared.attr("android.substitute", "attributeMapping", { "android:src": "app:srcCompat", "icon": "navigationIcon" });
squared.parseDocument({
element: document.body,
substitutableElements: [{
selector: "#content",
tag: "androidx.compose.ui.platform.ComposeView",
renderChildren: false
}],
enabledSubstitute: true,
include: ["android.substitute"]
});
<body>
<header style="height: 100px"></header>
<main id="content"
data-use="android.substitute"
data-android-substitute-tag="androidx.compose.ui.platform.ComposeView"
style="height: 300px; box-shadow: 10px 5px 5px black;">
</main>
<footer style="height: 80px"></footer>
</body>
Compose will remove child elements by default. You can preserve them by explictly using the renderChildren property. (data-android-substitute-render-children="true")
<div id="fragment"
data-use="android.substitute"
data-android-substitute-tag="androidx.fragment.app.FragmentContainerView"
data-android-substitute-render-children="false"
data-android-attr="name::com.github.fragment;tag::example">
</div>
You can also use "android.substitute" to create fragments within a layout similar to Compose.
Usually you do not render child elements when using Compose View. There are some cases where it can be used effectively to reproduce the desired layout.
squared.parseDocument({
element: document.body,
include: ["android.substitute"],
substitutableElements: [{
selector: "#navigation",
tag: "com.google.android.material.tabs.TabLayout",
tagChild: "com.google.android.material.tabs.TabItem",
tagChildAttr: {
android: {
layout_height: "match_parent"
}
},
renderChildren: true,
autoLayout: true
}]
});
<ul id="navigation"
data-use-android="android.substitute"
data-android-attr="layout_height::match_parent"
data-android-substitute-tag="com.google.android.material.tabs.TabLayout"
data-android-substitute-tag-child="com.google.android.material.tabs.TabItem"
data-android-substitute-tag-child-attr="layout_height::match_parent"
data-android-substitute-auto-layout="true">
<li>TAB 1</li>
<li>TAB 2</li>
<li>TAB 3</li>
</ul>
<com.google.android.material.tabs.TabLayout
android:id="@+id/navigation"
android:layout_height="match_parent"
android:layout_width="wrap_content">
<com.google.android.material.tabs.TabItem
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:text="@string/tab_1" />
<com.google.android.material.tabs.TabItem
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:text="@string/tab_2" />
<com.google.android.material.tabs.TabItem
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:text="@string/tab_3" />
</com.google.android.material.tabs.TabLayout>
Downloadable Fonts
Google Fonts are pre-installed and can be used without any additional configuration.
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.0'
implementation 'com.android.support:appcompat-v7:28.0.0'
}
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:theme="@style/AppTheme">
<meta-data android:name="preloaded_fonts" android:resource="@array/preloaded_fonts" />
</application>
</manifest>
await android.addFontProvider(
"com.google.android.gms.fonts",
"com.google.android.gms",
["MIIEqDCCA5CgAwIBAgIJANWFuGx9007...", "MIIEQzCCAyugAwIBAgIJAMLgh0Zk..."],
"https://www.googleapis.com/webfonts/v1/webfonts?key=1234567890"
);
squared.attr("android.resource.fonts", "installGoogleFonts", false);
Excluding Procedures / Applied Attributes
Most attributes can be excluded from the generated XML using the dataset feature in HTML. One or more can be applied to any tag using the OR "|" operator. These may cause warnings when you compile your project and should only be used when an extension has their custom attributes overwritten.
NOTE: Defining an element "id" will prevent it from being removed during the optimization phase.
<div data-exclude-section="DOM_TRAVERSE | EXTENSION | RENDER | ALL"
data-exclude-procedure="CONSTRAINT | LAYOUT | ALIGNMENT | ACCESSIBILITY | LOCALIZATION | CUSTOMIZATION | OPTIMIZATION | ALL"
data-exclude-resource="BOX_STYLE | BOX_SPACING | FONT_STYLE | VALUE_STRING | IMAGE_SOURCE | ASSET | ALL"
data-exclude-optimization="EXCLUDE | INHERIT | ALIGNMENT | POSITION | DIMENSION | MARGIN | PADDING | BASELINE | WHITESPACE | TRANSLATE | TRANSFORM | SCALING">
</div>
<div>
<span data-exclude-resource="FONT_STYLE">content</span>
<input id="cb1" type="checkbox" data-exclude-procedure="ACCESSIBILITY"><label for="cb1">checkbox text</label>
</div>
LICENSE
BSD 3-Clause