squared
TypeScript icon, indicating that this package has built-in type declarations

5.1.8 • Public • Published

squared 5.1

Installation

  • NodeJS 14 (Minimum)

NPX

> npm init

> npm i sqd-cli sqd-serve

> npx sqd init [--help]
# OR
> npx sqd init --public --local-serve # Same as squared-express

> npx sqd serve [--help]
# OR
> node serve.js [--help]
> npm init

> npm i squared sqd-serve

> mkdir dist html
> cp -r ./node_modules/squared/dist/* ./dist
> cp ./node_modules/squared/html/* ./html # optional
> cp ./node_modules/sqd-serve/config/[json|yaml]/* .

> npx serve [--help]

GitHub

> https://github.com/anpham6/squared/releases
# OR
> git clone https://github.com/anpham6/squared # unstable

> cd squared

> npm install

> npm run build
# OR
> npm run build:dev

> cd ..

> https://github.com/anpham6/squared-express/releases
# OR
> git clone https://github.com/anpham6/squared-express # unstable

> cd squared-express

> npm install

> npm run prod
> npm run deploy
# OR
> npm run dev

> cp ./dist/squared* ../squared
> cd ../squared
> nano squared.[json|yml] # configure
> node serve.js

> http://localhost:3000

Browser

Usage

Library files are in the "/dist" folder. A minimum of two files are required to run squared.

  1. squared
  2. squared-base - required: except vdom-lite
  3. squared-svg - optional
  4. framework (e.g. android | chrome | vdom | vdom-lite)
  5. 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".

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. Using "parseDocumentSync" is not recommended when your page has either 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 = 34; // Optional

    document.addEventListener("DOMContentLoaded", async () => {
        squared.setFramework(android, {/* settings */});

        await squared.parseDocument(): Node // document.body (default)
        // OR
        await squared.parseDocument(/* HTMLElement */, /* "fragment-id" */, /* ...etc */): Node[]
        // OR
        await squared.parseDocument(
            { // Custom settings do not affect other layouts
                element: document.body,
                projectId: "sqd1", // Default is "_"
                resourceQualifier: "land", // "res/*" folder suffix
                /* OR */
                resourceQualifier: {
                    suffix: "land", // Used for "true" or "undefined" groups (optional)
                    layout: true, // Will copy to "res/layout-land" when "suffix" is defined
                    string: undefined, // Will copy to default location "res/values" when "suffix" is undefined
                    font: false, // Will not copy anything to "res/font" or "res/font-land"
                    image: "hdpi", // Will copy to "res/drawable-hdpi"
                    video: "w720dp", // Will copy to "res/raw-w720dp"
                    audio: "w720dp", // Same as "video" and treated separately
                    animation: "v34", // Will copy to "res/anim-v34"
                    menu: "" // Will copy to default location "res/menu"
                    /* integer + fraction + array + color + dimension + style + theme = Same as "string" */
                },
                enabledMultiline: false,
                enabledSubstitute: true,
                include: ["android.substitute"], // Automatically removed after finalize
                exclude: ["squared.list", "squared.grid"], // Disabled only during parseDocument
                excludeQuery: [{
                    selector: "main > article", // Hide elements
                    /* OR */
                    resource: squared.base.lib.constant.NODE_RESOURCE.BOX_STYLE, // Exclusions during processing
                    procedure: squared.base.lib.constant.NODE_PROCEDURE.OPTIMIZATION,
                    section: squared.base.lib.constant.APP_SECTION.DOM_TRAVERSE
                }],
                customizationsBaseAPI: -1,
                observe(mutations, observer, settings) { // Uses MutationObserver
                    squared.reset(); // Required when calling "parseDocument" after a File action
                    squared.parseDocument(settings).then(() => {
                        squared.copyTo("/path/project", { modified: true } /* optional */).then(response => console.log(response));
                    });
                },
                afterCascade(sessionId, node) {/* Restore previous state */},
                beforeRender(layout: LayoutUI) {/* Edit initial values */},
                afterFinalize(node: NodeUI) {/* Edit controller values */}
            },
            { // Only "element" is required
                element: "fragment-1",
                projectId: "sqd1", // Implicit once projectId is not "_"
                resourceQualifier: "land",
                pathname: "app/src/main/res/layout-hdpi", // Will not be overridden by resourceQualifier "land"
                filename: "fragment.xml",
                baseLayoutAsFragment: true, // ["com.example.fragment", "fragment-tag"]
                beforeCascade(sessionId) {
                    document.getElementById("fragment-id").style.display = "block"; // Use inline styles
                }
            }
        );
        await squared.parseDocument({
            element: "fragment-2",
            projectId: "sqd2", // Explicit
            resourceQualifier: "port", // Will not conflict with projectId "sqd1"
            enabledFragment: true,
            fragmentableElements: [
                { selector: "main", name: "com.example.fragment", filename: "fragment.xml" }, // document.querySelector
                "main > article", // document.querySelectorAll (declarative double nested fragments are invalid)
            ],
            options: {
                "android.resource.fragment": {
                    dynamicNestedFragments: true // FragmentContainerView or FrameLayout as the container (name and tag are ignored)
                }
            }
        });
        // OR - Chromium
        squared.prefetch("css").then(() => squared.parseDocument()); // Cross-origin support
        Promise.all(
            squared.prefetch("css", true), // All stylesheets
            squared.prefetch("css", "./undetected.css", element.shadowRoot),
            squared.prefetch("svg", "http://embedded.example.com/icon.svg", "../images/android.svg")
        )
        .then(() => squared.parseDocument());

        // Modify attributes

        const body = squared.findDocumentNode(document.body);
        body.android("layout_width", "match_parent");
        body.lockAttr("android", "layout_width");

        await squared.close(/* projectId */); // Next call to parseDocument will reset project (optional)

        squared.kill("30s").then(result => {/* killed when result > 0 */}); // Abort next request in 30 seconds

        // File actions - implicitly calls "close"

        await squared.save(/* "sqd1" */, /* broadcastId | timeout */); // Uses defaults from settings
        // OR
        await squared.saveAs(/* archive filename */, { projectId: "sqd1" });
        await squared.saveAs(/* archive filename */, { timeout: 10 }); // Kills request if not complete in 10 seconds
        await squared.saveAs(/* archive filename */, { throwErrors: true }).catch(err => console.log(err)); // Will cancel partial archive download
        // OR
        await squared.copyTo(/* directory */, {/* options */});
        await squared.copyTo(/* directory */, { modified: true }); // Can be used with observe
        // OR
        await squared.appendTo(/* archive location */, {/* options */});

        // Other features

        squared.observe();
        // OR
        await squared.observeSrc(
            "link[rel=stylesheet]", // HTMLElement
            (ev, element) => {
                squared.reset();
                squared.parseDocument().then(() => squared.copyTo("/path/project"));
            },
            { port: 8080, secure: false, action: "reload" /* "hot" */, expires: "1h" } // squared.json: "observe"
        );

        squared.reset(/* projectId */); // Start new "parseDocument" session (optional)
    });
</script>

NOTE: Calling "save" or "copy" methods before the images have completely loaded can sometimes cause them to be excluded from the generated layout. In these cases you should use the "parseDocument" promise method "then" to set a callback for your commands.

Example: chrome

You have the same features as VDOM framework but you can also bundle assets using HTML syntax and the provided Express server. It is adequate for most applications and gives you the ability to see your application first and to build it last.

<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, {/* settings */});

        await squared.save(); // Uses defaults from settings
        // OR
        await squared.saveAs(/* archive filename */, {/* options */});
        // OR
        await squared.copyTo(/* directory */, {/* options */});
        // OR
        await squared.appendTo(/* archive location */, {/* options */});

        // Observe
        await squared.copyTo(/* directory */, { useOriginalHtmlPage: false, observe: /* Same as Android */ | true /* Auto-reload */}).then(() => squared.observe());
    });
</script>

Example: vdom

VDOM is a minimal framework (50kb gzipped) and is especially useful for debugging. 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, {/* settings */});

        const element = squared.querySelector("body", true /* synchronous */);
        // OR
        const elements = await squared.querySelectorAll("*");
        // OR
        const element = squared.fromElement(document.body, true /* synchronous */);
        // OR
        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: 34,
    supportRTL: true,
    supportNegativeLeftTop: true,
    preloadImages: true,
    preloadFonts: true,
    preloadLocalFonts: true, // Chromium
    preloadCustomElements: true,
    enabledSVG: true,
    enabledMultiline: true,
    enabledViewModel: true,
    enabledIncludes: false,
    enabledFragment: false,
    enabledSubstitute: false,
    enabledCompose: false,
    dataBindableElements: [], // { selector, attr, expression, namespace?, twoWay? } (see Data Binding section)
    includableElements: [], // { selectorStart, selectorEnd, pathname?, filename?, merge?, viewModel? }
    substitutableElements: [], // { selector, tag, tagChild?, renderChildren?, autoLayout? }
    fragmentableElements: [], // selector | ExtensionFragmentElement
    composableElements: [], // selector or property (see Jetpack Compose section)
    baseLayoutAsFragment: false | "fragment-name" | ["fragment-name", "fragment-tag"] | { selector, pathname?, filename?, name?, tag? }, // ExtensionFragmentElement
    baseLayoutToolsIgnore: "", // Android Studio (e.g. "TooManyViews, HardcodedText")
    fontMeasureAdjust: 0.75, // thicker < 0 | thinner > 0 (data-android-font-measure-adjust)
    lineHeightAdjust: 1.1, // shorter < 1 | taller > 1 (data-android-line-height-adjust)
    preferMaterialDesign: false | "MaterialComponents" | "Material3", // Default is "Material3"
    createDownloadableFonts: true,
    createElementMap: false,
    pierceShadowRoot: true,
    lockElementSettings: true,
    customizationsBaseAPI: 0, // 0 - All | -1 - None
    customizationsBaseAPI: [0, 33, 34], // Multiple
    removeDeprecatedAttributes: true, // Remove all
    removeDeprecatedAttributes: ["enabled", "singleLine"], // Remove all except "enabled" + "singleLine"
    removeUnusedResourceViewId: false,
    idNamingStyle: "android",
    idNamingStyle: "html", // Use element tagName
    idNamingStyle: {
        "__default__": "html", // Optional
        "DIV": "comments", // HTML is uppercase (comments_1 then comments_2)
        "svg": ["vector", 0], // SVG elements areis lowercase (vector_0 then vector_1)
        "#text": "text", // Plain text
        "::first-letter": "dropcap", // Pseudo element
        "main > section": ["content", 1, 2], // content_1 then content_3
        "form input[type=submit]": function(node) {
            return 'submit_' + node.id;
        }
    },
    customizationsOverwritePrivilege: true,
    outputMainFileName: "activity_main.xml",
    outputFragmentFileName: "fragment_main.xml",
    /* Project - parseDocument (first only) */
    resourceQualifier: "", // "land" -> "res/layout-land" | "port" -> "res/layout-port" (appended to every "res" folder)
    manifestPackage: "", // OR: RequestData<{ namespace: "android.application.id" }>
    manifestLabelAppName: "android",
    manifestThemeName: "AppTheme",
    manifestParentThemeName: "Theme.AppCompat.Light.NoActionBar",
    manifestActivityName: ".MainActivity",
    outputDocumentEditing: true,
    outputDocumentCSS: [], // CSS properties to be processed at server (e.g. "boxShadow")
    outputDirectory: "app/src/main",
    createManifest: false, // Update AndroidManifest.xml
    createBuildDependencies: false | "ktx" | "baseline-profile" | ["ktx", "baseline-profile"], // Update build.gradle

    // Not customizable with parseDocument
    builtInExtensions: [
        "squared.accessibility",
        "android.delegate.background",
        "android.delegate.negative-x",
        "android.delegate.positive-x",
        "android.delegate.max-width-height",
        "android.delegate.percent",
        "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",

        /* EXCLUDED (breaks layout) */
        "android.resource.includes", // enabledIncludes
        "android.substitute", // enabledSubstitute
        "android.resource.fragment", // enabledFragment
        "jetpack.compose.view" // enabledCompose
    ],
    compressImages: false, // TinyPNG API Key <https://tinypng.com/developers>
    convertImages: "", // png | jpeg | webp | gif | bmp
    showAttributes: true,
    showComments: false | ["boxShadow"] | { self: ["boxShadow"], nextSibling: ["marginBottom"], previousSibling: ["marginTop"], parent: ["position", "top", "left"] }, // TODO in layout.xml
    showComments: { include: { tagName: true | ["button"], attributes: true | ["style"], dataset: false, bounds: true }, self: ["boxShadow", ".className"] },
    showErrorMessages: false,
    convertPixels: "dp", // "sp" | "pt" | "in" | "mm"
    convertLineHeight: "sp", // "dp" | "pt" | "in" | "mm"
    insertSpaces: 4,
    outputDocumentHandler: "android",
    outputEmptyCopyDirectory: false, // Sub directories within target directory (OR: RequestData<{ emptyDir: 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", // tar | 7z | gz
    outputArchiveCache: false // Downloadable URL in ResponseData<downloadUrl>
};

// Optional
squared.settings = {
    resolutionDPI: 160, // 320dpi = 2560x1600
    resolutionScreenWidth: 1280,
    resolutionScreenHeight: 800,
    framesPerSecond: 60,
    formatUUID: "8-4-4-4-12", // UUID: 8-4-[12345]3-[89ab]3-12
    formatDictionary: "0123456789abcdef",
    outputConfigName: "sqd.config",
    observePort: 8080,
    observeSecurePort: 8443,
    observeExpires: "1h", // Server defaults will be used
    broadcastPort: 3080,
    broadcastSecurePort: 3443
};

Example: chrome

squared.settings = {
    preloadImages: false,
    preloadFonts: false,
    preloadLocalFonts: false,
    preloadCustomElements: false,
    excludePlainText: true,
    createElementMap: true,
    pierceShadowRoot: true,
    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
};

// Optional (Same as Android)

Example: vdom

squared.settings = {
    createElementMap: true,
    pierceShadowRoot: 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.

// Save
squared.setFramework(android, { compressImages: true }, "android-example");

// Load
squared.setFramework(android, "android-example");
// Save
await squared.copyTo("/path/project", {/* options will be saved */}, "copy-example", true); // Will overwrite and not merge with previously saved settings

// Load
await squared.copyTo("/path/project", {/* takes precedence */}, "http://localhost:3000/copy-to/base-config.json"); // Object.assign({ base-config.json }, options)
await squared.copyTo("/path/project", {/* takes precedence */}, "copy-example"); // Object.assign({ copy-example }, options)

await squared.copyTo("/path/project", "http://localhost:3000/copy-to/base-config.json"); // options = { base-config.json }
await squared.copyTo("/path/project", "copy-example"); // options = { copy_example }

Public Properties and Methods

There is no official documentation for this project. The entire source code including TypeScript definitions are available on GitHub if you need further clarification.

.settings // See user preferences section

setFramework(app: {}, options?: PlainObject, setting?: string, cache?: boolean) // Install application interpreter
setFramework(app: {}, loadName: string, cache?: boolean) // Load settings from local storage

// http - hostname(:port)? | https - hostname:443
setHostname(value: string /* http(s)://hostname(:port) */) // Use another cors-enabled server for processing files (--cors <origin>)

setEndpoint(name: string, value: string) // Set alternate pathname for API v1 functions (ASSETS_COPY | ASSETS_ARCHIVE | LOADER_DATA | THREADS_KILL | WEBSOCKET_OBSERVE)
setLocalAddress(...values: (string | URL | Location)[]) // Additional hostnames which are interpreted as localhost (e.g. http://127.0.0.1)

prefetch(type: "css" | "javascript" | "image" | "svg", all?: boolean, ...targets: unknown[]) // Cross-origin support for CSS

parseDocument(...elements: (HTMLElement | string | ElementSettings)[]) // See installation section (Promise)
parseDocumentSync(...elements: (HTMLElement | string | ElementSettings)[]) // Skips preloadImages and preloadFonts (synchronous)

latest(count?: number) // Most recent parseDocument session ids (1 newest / -1 oldest: string, other: string[])

auth(token: string) // Set JWT authorization token for all requests

save(projectId?: string) // Save current session to a new archive using default settings
save(projectId?: string, broadcastId?: string)
save(projectId?: string, timeout?: number)

close(projectId?: string) // Close current session
reset(projectId?: string) // Clear cache and reopen new session
clear() // Clear all data stored in memory

toString() // Current framework loaded

add(...names: (string | Extension | ExtensionRequestObject)[]) // See extension configuration section
remove(...names: (string | Extension)[]) // Remove extensions by namespace or control
get(...names: string[]) // Retrieve extensions by namespace
attr(name: string | Extension, attrName: string, value?: unknown) // Set or get extension options attribute value
apply(name: string | Extension, options: PlainObject, setting?: string) // See extension configuration section

extend(functionMap: {}, framework?: /* 0 - ALL | 1 - vdom | 2 - android | 4 - chrome */) // Add extension functions and properties to Node prototype

observe(value?: boolean | MutationObserverInit) // Start after DOM and third-party libraries initialization (https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe)
broadcast(callback: BroadcastMessageCallback, options: FileBroadcastOptions | string) // Redirect stdout messages to DevTools console

// Promise (Recommended "cache": createElementMap - true)

getElementById(value: string, sync?: boolean, cache?: boolean) // sync - false | cache - true (default)
querySelector(value: string, sync?: boolean, cache?: boolean)
querySelectorAll(value: string, sync?: boolean, cache?: boolean)

fromElement(element: HTMLElement | string, sync?: boolean, cache?: boolean) // sync - false | cache - false (default)
fromNode(node: Node, sync?: boolean, cache?: boolean)
findDocumentNode(element: HTMLElement | string /* querySelector | elementId | controlId */, all?: boolean) // Use before saving to modify internal Node attributes

observeSrc(element: HTMLElement | string /* querySelector */, callback: WebSocketMessageChange, options?: FileObserveOptions) // Can be used to watch any element with externally hosted files (src/href)
observeSrc(element: HTMLElement | string, options: FileObserveOptions) // Uses location.reload (reload - true)

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) // Save current session as a new archive
saveFiles(filename: string, options: {}, setting?: string, overwrite?: boolean) // Create new archive from FileAsset[]

// Required (local archives): --disk-read | --unc-read | --access-all (command-line)

appendTo(pathname: string, options?: {}, setting?: string, overwrite?: boolean) // Create new archive from a preexisting archive and current session
appendFiles(pathname: string, options: {}, setting?: string, overwrite?: boolean) // Create new archive from a preexisting archive and FileAsset[]

// Required (all): --disk-write | --unc-write | --access-all (command-line)

copyTo(pathname: string | string[], options?: {}, setting?: string, overwrite?: boolean) // Copy current session to local
copyFiles(pathname: string | string[], options: {}, setting?: string, overwrite?: boolean) // Copy FileAsset[] to local

kill(pid: number, timeout?: number) // Use -1 or options.pid (set by system) + seconds
kill(timeout: string)
kill() // Terminate previous request
kill(0) // By username (auth required)
kill(-1, 10) // Terminate previous request in 10 seconds
kill(NaN, 10) // Terminate in 10 seconds (next request) ("timeout" is required)
kill("10s") // Only "s" + "ms" (next request)

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.altId: 3
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");
// OR
squared.setHostname(); // Reset to window.location (e.g. localhost:3000)

await squared.saveAs("chrome.zip"); // Current browser
// OR
await squared.copyTo("/path/project"); // Remote server

Broadcasting

Console messages (stdout) can be sent to the browser console instead through DevTools.

squared.broadcast(result => { console.log(result.value); }, "111-111-111"); // System messages from squared-express
squared.broadcast(result => { console.log(result.value); }, "222-222-222"); // Messages from "sqd1" project
squared.broadcast(result => { console.log(result.value); }, { socketId: "333-333-333", socketKey: "socket_id" }); // Messages sent from another channel (default is "socketId")

await squared.copyTo("/path/project/sqd1", {
    projectId: "sqd1",
    log: { useColor: true }, // Chromium
    broadcastId: "222-222-222" // Specific use alias for "socketId"
});

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.

// Create an extension
class Sample extends squared.base.Extension {
    options = {
        attributeName: [];
    };

    constructor(name, framework = 0, options = {}) { // 0 - ALL | 1 - vdom | 2 - android | 4 - chrome (framework)
        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]);
        }
    }
}

// Install an extension
const sample = new Sample("widget.example.com", 0, {/* Same as configure */});
squared.add(sample);
// OR
squared.add([sample, {/* config */}]);

// Configure an extension
squared.attr("widget.example.com", "attributeName", ["width", "height"]); // typeof is enforced and will only set existing attributes

// Add project data
const ext = squared.get("widget.example.com");
ext.project.set(element, await fetch(url), "sqd1" /* projectId */); // Same as Map interface with optional "projectId" argument

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) // Object data for layout bindings
android.setViewModelByProject(data: {}, projectId?: string)
android.removeObserver(element: HTMLElement) // Disconnect an observed element from "parseDocument"
android.addXmlNs(name: string, uri: string) // Add global namespaces for third-party controls
android.addDependency(group: string, name: string, version?: string, type?: number) // Add application dependency implementation (build.gradle)
android.addDependencyByProject(projectId: string, group: string, name: string, version?: string, type?: number) // DEPENDENCY_TYPE: 0 - implementation 1 - api 2 - compileOnly 3 - compileOnlyApi 4 - runtimeOnly 5 - testImplementation 8 - androidTestImplementation
android.customize(build: number, tagNameOrWidget: string, options: {}) // Global attributes applied to specific views
android.loadCustomizations(name: string) // Load customizations from Local Storage
android.saveCustomizations(name: string) // Save "customize" data into Local Storage (includes xmlns)
android.resetCustomizations() // All session customizations are deleted
android.addFontProvider(authority: string, package: string, certs: string[], webFonts: string | {}) // Add additional Web fonts (Google Fonts already included)
android.setResolutionByDeviceName(value: string) // Settings prefixed with "resolution" (e.g. Pixel C)
android.getLocalSettings() // Modify controller styles and parsing rules
// NOTE: squared.settings.targetAPI is always parsed (Except: customizationsBaseAPI = -1)

android.customize(android.lib.constant.BUILD_VERSION.ALL /* 0 */, "Button", {
    android: {
        minWidth: "35px",
        minHeight: "25px"
    },
    "_": { // Non-namespaced attributes
        style: "@style/Widget.Material3.Button.TextButton"
    }
});

android.customize(android.lib.constant.BUILD_VERSION.KITKAT /* 19 */, "svg", {
    android: {
        "[src]": "app:srcCompat" // Change namespace to "app"
    }
});

// Local Storage
android.saveCustomizations("customize-example"); // Save at least once in one layout

android.loadCustomizations("customize-example"); // Load in any other layout
android.addXmlNs("tools", "http://schemas.android.com/tools");

android.customize(16 /* Jelly Bean */, "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, /* name */);
    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.

// NOTE: latest(undefined = 1): string (1: most recent sessionId | -1: first sessionId)

squared.parseDocument(/* "mainview" */, /* "subview" */).then(nodes => {
    const sessions = squared.latest(2); // ["1", "2", "3"] => ["2", "3"]
    android.setViewModel(
        {
            import: ["java.util.Map", "java.util.List"],
            variable: [
                { name: "user", type: "com.example.User" },
                { name: "list", type: "List&lt;String>" },
                { name: "map", type: "Map&lt;String, String>" },
                { name: "index", type: "int" },
                { name: "key", type: "String" }
            ]
        },
        sessions[0] // nodes[1].sessionId
    );
    android.setViewModel(
        {
            import: ["java.util.Map"],
            variable: [
                { name: "map", type: "Map&lt;String, String>" }
            ]
        },
        sessions[1] // nodes[2].sessionId
    );
});

squared.parseDocument({
    element: "main",
    enabledViewModel: true,
    dataBindableElements: [
        {
            selector: "#first_name",
            namespace: "android", // "android" is default
            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&lt;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" /> <!-- "=" for two-way binding -->
</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&lt;String&gt;" />
        <variable name="map" type="Map&lt;String, String&gt;" />
        <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 are also 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> <!-- viewModel -->
</div>
android.setViewModelByProject({ variable: [{ name: "exampleData", type: "com.example.ExampleData" }] }, "sqd1"); // Default is "_"

squared.parseDocument({
    element: document.body,
    projectId: "sqd1", // Affects all layouts in same project
    enabledIncludes: true,
    includableElements: [
        {
            selectorStart: "#item2",
            selectorEnd: "#item4",
            pathname: "app/src/main/res/layout-land",
            filename: "filename1.xml",
            merge: true // Multiple elements will auto-merge
        },
        {
            selectorStart: "#item5",
            selectorEnd: "#item5",
            filename: "filename2",
            viewModel: "exampleData" // One element only (merge=false)
        }
    ]
});

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>
<!-- res/layout/activity_main.xml -->

<merge>
    <TextView>Item 2</TextView>
    <TextView>Item 3</TextView>
    <TextView>Item 4</TextView>
</merge>
<!-- res/layout-land/filename1.xml -->

<layout>
    <data>
        <variable name="exampleData" type="com.example.ExampleData" />
    </data>
    <TextView>Item 5</TextView>
</layout>
<!-- res/layout/filename2.xml -->

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.

NOTE: "data-pathname-android" AND "data-filename-android" can also be used with any "parseDocument" base element.

Redirecting Output Location

Sometimes it is necessary to append elements into other containers when trying to design a UI which will 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>
    <!-- span -->
</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"); // querySelector is supported
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(); // Required
}

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"];
squared.settings.createBuildDependencies = true; // Optional

You can also do it using the "android.substitute" extension directly inside the HTML element.

// android.substitute is only used here to demonstrate using extensions

squared.add(["android.substitute", {
    element: {
        content: { android: { layout_width: "match_parent" } }
    }
}]);

const items = squared.attr("android.substitute", "viewAttributes");
items.push("hint", "buttonTint");
/* OR */
squared.attr("android.substitute", "viewAttributes", items.concat(["hint", "buttonTint"])); // Attributes to preserve (default is "android.view.View")
squared.attr("android.substitute", "attributeMapping", { "android:src": "app:srcCompat", "icon": "navigationIcon" /* android */});

squared.parseDocument({
    element: document.body,
    substitutableElements: [{
        selector: "#content",
        tag: "androidx.compose.ui.platform.ComposeView",
        renderChildren: false
    }],

    // Some extensions have convenience properties
    enabledSubstitute: true,
    /* OR */
    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;">
        <!-- Interior elements are not rendered -->
    </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">
    <!-- Interior elements are not rendered -->
</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"], // OR: settings.enabledSubstitute
    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.

NOTE: WOFF2 format is supported as a local resource font.

<!-- build.gradle -->
dependencies {
    implementation 'androidx.appcompat:appcompat:1.6.0' <!-- createBuildDependencies = true -->
    <!-- OR -->
    implementation 'com.android.support:appcompat-v7:28.0.0'
}

<!-- AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <!-- createManifest = true -->
    <application android:theme="@style/AppTheme">
        <meta-data android:name="preloaded_fonts" android:resource="@array/preloaded_fonts" />
    </application>
</manifest>
// https://developers.google.com/fonts/docs/developer_api

await android.addFontProvider(
    "com.google.android.gms.fonts",
    "com.google.android.gms",
    ["MIIEqDCCA5CgAwIBAgIJANWFuGx9007...", "MIIEQzCCAyugAwIBAgIJAMLgh0Zk..."],
    "https://www.googleapis.com/webfonts/v1/webfonts?key=1234567890" // JSON object is synchronous
);
/* OR */
squared.attr("android.resource.fonts", "installGoogleFonts", false); // Use browser and local fonts only

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">
</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

Apache 2.0

Dependencies (28)

Dev Dependencies (1)

Package Sidebar

Install

npm i squared

Weekly Downloads

377

Version

5.1.8

License

Apache 2.0

Unpacked Size

18.8 MB

Total Files

102

Last publish

Collaborators

  • anpham6