@erickxavier/no-js 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/filters.js ADDED
@@ -0,0 +1,137 @@
1
+ // ═══════════════════════════════════════════════════════════════════════
2
+ // BUILT-IN FILTERS
3
+ // ═══════════════════════════════════════════════════════════════════════
4
+
5
+ import { _filters } from "./globals.js";
6
+
7
+ // Text
8
+ _filters.uppercase = (v) => String(v ?? "").toUpperCase();
9
+ _filters.lowercase = (v) => String(v ?? "").toLowerCase();
10
+ _filters.capitalize = (v) =>
11
+ String(v ?? "").replace(/\b\w/g, (c) => c.toUpperCase());
12
+ _filters.truncate = (v, len = 100) => {
13
+ const s = String(v ?? "");
14
+ return s.length > len ? s.slice(0, len) + "..." : s;
15
+ };
16
+ _filters.trim = (v) => String(v ?? "").trim();
17
+ _filters.stripHtml = (v) => String(v ?? "").replace(/<[^>]*>/g, "");
18
+ _filters.slugify = (v) =>
19
+ String(v ?? "")
20
+ .toLowerCase()
21
+ .replace(/[^a-z0-9]+/g, "-")
22
+ .replace(/^-|-$/g, "");
23
+ _filters.nl2br = (v) => String(v ?? "").replace(/\n/g, "<br>");
24
+ _filters.encodeUri = (v) => encodeURIComponent(String(v ?? ""));
25
+
26
+ // Numbers
27
+ _filters.number = (v, decimals = 0) => {
28
+ const n = Number(v);
29
+ if (isNaN(n)) return v;
30
+ return n.toLocaleString(undefined, {
31
+ minimumFractionDigits: decimals,
32
+ maximumFractionDigits: decimals,
33
+ });
34
+ };
35
+ _filters.currency = (v, code = "USD") => {
36
+ const n = Number(v);
37
+ if (isNaN(n)) return v;
38
+ try {
39
+ return n.toLocaleString(undefined, { style: "currency", currency: code });
40
+ } catch {
41
+ return `${code} ${n.toFixed(2)}`;
42
+ }
43
+ };
44
+ _filters.percent = (v, decimals = 0) => {
45
+ const n = Number(v);
46
+ if (isNaN(n)) return v;
47
+ return (n * 100).toFixed(decimals) + "%";
48
+ };
49
+ _filters.filesize = (v) => {
50
+ const n = Number(v);
51
+ if (isNaN(n)) return v;
52
+ const units = ["B", "KB", "MB", "GB", "TB"];
53
+ let i = 0;
54
+ let size = n;
55
+ while (size >= 1024 && i < units.length - 1) {
56
+ size /= 1024;
57
+ i++;
58
+ }
59
+ return size.toFixed(i > 0 ? 1 : 0) + " " + units[i];
60
+ };
61
+ _filters.ordinal = (v) => {
62
+ const n = Number(v);
63
+ if (isNaN(n)) return v;
64
+ const s = ["th", "st", "nd", "rd"];
65
+ const mod = n % 100;
66
+ return n + (s[(mod - 20) % 10] || s[mod] || s[0]);
67
+ };
68
+
69
+ // Arrays
70
+ _filters.count = (v) => (Array.isArray(v) ? v.length : 0);
71
+ _filters.first = (v) => (Array.isArray(v) ? v[0] : v);
72
+ _filters.last = (v) => (Array.isArray(v) ? v[v.length - 1] : v);
73
+ _filters.join = (v, sep = ", ") => (Array.isArray(v) ? v.join(sep) : v);
74
+ _filters.reverse = (v) => (Array.isArray(v) ? [...v].reverse() : v);
75
+ _filters.unique = (v) => (Array.isArray(v) ? [...new Set(v)] : v);
76
+ _filters.pluck = (v, key) => (Array.isArray(v) ? v.map((i) => i?.[key]) : v);
77
+ _filters.sortBy = (v, key) => {
78
+ if (!Array.isArray(v)) return v;
79
+ const desc = key?.startsWith("-");
80
+ const k = desc ? key.slice(1) : key;
81
+ return [...v].sort((a, b) => {
82
+ const va = a?.[k],
83
+ vb = b?.[k];
84
+ const r = va < vb ? -1 : va > vb ? 1 : 0;
85
+ return desc ? -r : r;
86
+ });
87
+ };
88
+ _filters.where = (v, key, val) =>
89
+ Array.isArray(v) ? v.filter((i) => i?.[key] === val) : v;
90
+
91
+ // Date
92
+ _filters.date = (v, fmt = "short") => {
93
+ const d = new Date(v);
94
+ if (isNaN(d)) return v;
95
+ const opts =
96
+ fmt === "long"
97
+ ? { dateStyle: "long" }
98
+ : fmt === "full"
99
+ ? { dateStyle: "full" }
100
+ : { dateStyle: "short" };
101
+ return d.toLocaleDateString(undefined, opts);
102
+ };
103
+ _filters.datetime = (v) => {
104
+ const d = new Date(v);
105
+ if (isNaN(d)) return v;
106
+ return d.toLocaleString();
107
+ };
108
+ _filters.relative = (v) => {
109
+ const d = new Date(v);
110
+ if (isNaN(d)) return v;
111
+ const diff = (Date.now() - d.getTime()) / 1000;
112
+ if (diff < 60) return "just now";
113
+ if (diff < 3600) return Math.floor(diff / 60) + "m ago";
114
+ if (diff < 86400) return Math.floor(diff / 3600) + "h ago";
115
+ if (diff < 2592000) return Math.floor(diff / 86400) + "d ago";
116
+ return d.toLocaleDateString();
117
+ };
118
+ _filters.fromNow = (v) => {
119
+ const d = new Date(v);
120
+ if (isNaN(d)) return v;
121
+ const diff = (d.getTime() - Date.now()) / 1000;
122
+ if (diff < 0) return _filters.relative(v);
123
+ if (diff < 60) return "in a moment";
124
+ if (diff < 3600) return "in " + Math.floor(diff / 60) + "m";
125
+ if (diff < 86400) return "in " + Math.floor(diff / 3600) + "h";
126
+ return "in " + Math.floor(diff / 86400) + "d";
127
+ };
128
+
129
+ // Utility
130
+ _filters.default = (v, def = "") => (v == null || v === "" ? def : v);
131
+ _filters.json = (v, indent = 2) => JSON.stringify(v, null, indent);
132
+ _filters.debug = (v) => {
133
+ console.log("[No.JS debug]", v);
134
+ return v;
135
+ };
136
+ _filters.keys = (v) => (v && typeof v === "object" ? Object.keys(v) : []);
137
+ _filters.values = (v) => (v && typeof v === "object" ? Object.values(v) : []);
package/src/globals.js ADDED
@@ -0,0 +1,58 @@
1
+ // ═══════════════════════════════════════════════════════════════════════
2
+ // SHARED STATE & UTILITIES
3
+ // ═══════════════════════════════════════════════════════════════════════
4
+
5
+ export const _config = {
6
+ baseApiUrl: "",
7
+ headers: {},
8
+ timeout: 10000,
9
+ retries: 0,
10
+ retryDelay: 1000,
11
+ credentials: "same-origin",
12
+ csrf: null,
13
+ cache: { strategy: "none", ttl: 300000 },
14
+ templates: { cache: true },
15
+ router: { mode: "history", base: "/", scrollBehavior: "top" },
16
+ i18n: { defaultLocale: "en", fallbackLocale: "en", detectBrowser: false },
17
+ debug: false,
18
+ devtools: false,
19
+ csp: null,
20
+ sanitize: true,
21
+ };
22
+
23
+ export const _interceptors = { request: [], response: [] };
24
+ export const _eventBus = {};
25
+ export const _stores = {};
26
+ export const _storeWatchers = new Set();
27
+ export const _filters = {};
28
+ export const _validators = {};
29
+ export const _cache = new Map();
30
+ export const _refs = {};
31
+ export let _routerInstance = null;
32
+
33
+ export function setRouterInstance(r) {
34
+ _routerInstance = r;
35
+ }
36
+
37
+ export function _log(...args) {
38
+ if (_config.debug) console.log("[No.JS]", ...args);
39
+ }
40
+
41
+ export function _warn(...args) {
42
+ console.warn("[No.JS]", ...args);
43
+ }
44
+
45
+ export function _notifyStoreWatchers() {
46
+ for (const fn of _storeWatchers) fn();
47
+ }
48
+
49
+ export function _watchExpr(expr, ctx, fn) {
50
+ ctx.$watch(fn);
51
+ if (typeof expr === "string" && expr.includes("$store")) {
52
+ _storeWatchers.add(fn);
53
+ }
54
+ }
55
+
56
+ export function _emitEvent(name, data) {
57
+ (_eventBus[name] || []).forEach((fn) => fn(data));
58
+ }
package/src/i18n.js ADDED
@@ -0,0 +1,36 @@
1
+ // ═══════════════════════════════════════════════════════════════════════
2
+ // i18n SYSTEM
3
+ // ═══════════════════════════════════════════════════════════════════════
4
+
5
+ import { _config } from "./globals.js";
6
+
7
+ export const _i18n = {
8
+ locale: "en",
9
+ locales: {},
10
+ t(key, params = {}) {
11
+ const messages =
12
+ _i18n.locales[_i18n.locale] ||
13
+ _i18n.locales[_config.i18n.fallbackLocale] ||
14
+ {};
15
+ let msg = key.split(".").reduce((o, k) => o?.[k], messages);
16
+ if (msg == null) return key;
17
+
18
+ // Pluralization: "one item | {count} items"
19
+ if (
20
+ typeof msg === "string" &&
21
+ msg.includes("|") &&
22
+ params.count != null
23
+ ) {
24
+ const forms = msg.split("|").map((s) => s.trim());
25
+ msg = Number(params.count) === 1 ? forms[0] : forms[1] || forms[0];
26
+ }
27
+
28
+ // Interpolation: {name}
29
+ if (typeof msg === "string") {
30
+ msg = msg.replace(/\{(\w+)\}/g, (_, k) =>
31
+ params[k] != null ? params[k] : "",
32
+ );
33
+ }
34
+ return msg;
35
+ },
36
+ };
package/src/index.js ADDED
@@ -0,0 +1,189 @@
1
+ // ═══════════════════════════════════════════════════════════════════════
2
+ // No.JS — Module Entry Point
3
+ // For npm/ESM/CJS consumers: import NoJS from 'nojs'
4
+ // ═══════════════════════════════════════════════════════════════════════
5
+
6
+ // Core modules
7
+ import {
8
+ _config,
9
+ _filters,
10
+ _validators,
11
+ _interceptors,
12
+ _eventBus,
13
+ _stores,
14
+ _refs,
15
+ _routerInstance,
16
+ setRouterInstance,
17
+ _log,
18
+ } from "./globals.js";
19
+ import { _i18n } from "./i18n.js";
20
+ import { createContext } from "./context.js";
21
+ import { evaluate, resolve } from "./evaluate.js";
22
+ import { findContext, _loadRemoteTemplates, _loadRemoteTemplatesPhase1, _loadRemoteTemplatesPhase2, _processTemplateIncludes } from "./dom.js";
23
+ import { registerDirective, processTree } from "./registry.js";
24
+ import { _createRouter } from "./router.js";
25
+
26
+ // Side-effect imports: register built-in filters
27
+ import "./filters.js";
28
+
29
+ // Side-effect imports: register all built-in directives
30
+ import "./directives/state.js";
31
+ import "./directives/http.js";
32
+ import "./directives/binding.js";
33
+ import "./directives/conditionals.js";
34
+ import "./directives/loops.js";
35
+ import "./directives/styling.js";
36
+ import "./directives/events.js";
37
+ import "./directives/refs.js";
38
+ import "./directives/validation.js";
39
+ import "./directives/i18n.js";
40
+
41
+ // ═══════════════════════════════════════════════════════════════════════
42
+ // PUBLIC API
43
+ // ═══════════════════════════════════════════════════════════════════════
44
+
45
+ function _getDefaultRoutePath() {
46
+ if (typeof window === "undefined") return null;
47
+ const routerCfg = _config.router || {};
48
+ if (routerCfg.mode === "hash") {
49
+ return window.location.hash.slice(1) || "/";
50
+ }
51
+ const base = (routerCfg.base || "/").replace(/\/$/, "");
52
+ return window.location.pathname.replace(base, "") || "/";
53
+ }
54
+
55
+ const NoJS = {
56
+ get baseApiUrl() {
57
+ return _config.baseApiUrl;
58
+ },
59
+ set baseApiUrl(v) {
60
+ _config.baseApiUrl = v;
61
+ },
62
+
63
+ config(opts = {}) {
64
+ // Save nested objects before shallow assign overwrites them
65
+ const prevHeaders = { ..._config.headers };
66
+ const prevCache = { ..._config.cache };
67
+ const prevTemplates = { ..._config.templates };
68
+ const prevRouter = { ..._config.router };
69
+ const prevI18n = { ..._config.i18n };
70
+ Object.assign(_config, opts);
71
+ if (opts.headers)
72
+ _config.headers = { ...prevHeaders, ...opts.headers };
73
+ if (opts.csrf) _config.csrf = opts.csrf;
74
+ if (opts.cache) _config.cache = { ...prevCache, ...opts.cache };
75
+ if (opts.templates) _config.templates = { ...prevTemplates, ...opts.templates };
76
+ if (opts.router) _config.router = { ...prevRouter, ...opts.router };
77
+ if (opts.i18n) {
78
+ _config.i18n = { ...prevI18n, ...opts.i18n };
79
+ _i18n.locale = opts.i18n.defaultLocale || _i18n.locale;
80
+ }
81
+ },
82
+
83
+ async init(root) {
84
+ if (typeof document === "undefined") return;
85
+ root = root || document.body;
86
+ _log("Initializing...");
87
+
88
+ // Inline template includes (e.g. skeletons) — synchronous, before any fetch
89
+ _processTemplateIncludes(root);
90
+
91
+ // Determine active route path for phase 1 prioritization
92
+ const defaultRoutePath = _getDefaultRoutePath();
93
+
94
+ // Phase 1 (blocking): priority + non-route + default route templates
95
+ await _loadRemoteTemplatesPhase1(defaultRoutePath);
96
+
97
+ // Check for route-view outlets to activate router
98
+ if (document.querySelector("[route-view]")) {
99
+ setRouterInstance(_createRouter());
100
+ }
101
+
102
+ processTree(root); // ← first paint happens here
103
+
104
+ // Init router after tree is processed
105
+ if (_routerInstance) await _routerInstance.init();
106
+
107
+ _log("Initialized.");
108
+
109
+ // Phase 2 (non-blocking): background preload remaining route templates
110
+ _loadRemoteTemplatesPhase2();
111
+
112
+ // DevTools integration
113
+ if (_config.devtools && typeof window !== "undefined") {
114
+ window.__NOJS_DEVTOOLS__ = {
115
+ stores: _stores,
116
+ config: _config,
117
+ refs: _refs,
118
+ router: _routerInstance,
119
+ filters: Object.keys(_filters),
120
+ validators: Object.keys(_validators),
121
+ version: NoJS.version,
122
+ };
123
+ _log("DevTools enabled — access via window.__NOJS_DEVTOOLS__");
124
+ }
125
+ },
126
+
127
+ // Register custom directive
128
+ directive(name, handler) {
129
+ registerDirective(name, handler);
130
+ },
131
+
132
+ // Register custom filter
133
+ filter(name, fn) {
134
+ _filters[name] = fn;
135
+ },
136
+
137
+ // Register custom validator
138
+ validator(name, fn) {
139
+ _validators[name] = fn;
140
+ },
141
+
142
+ // i18n
143
+ i18n(opts) {
144
+ if (opts.locales) _i18n.locales = opts.locales;
145
+ if (opts.defaultLocale) _i18n.locale = opts.defaultLocale;
146
+ if (opts.fallbackLocale) _config.i18n.fallbackLocale = opts.fallbackLocale;
147
+ if (opts.detectBrowser) {
148
+ const browserLang =
149
+ typeof navigator !== "undefined" ? navigator.language : "en";
150
+ if (_i18n.locales[browserLang]) _i18n.locale = browserLang;
151
+ }
152
+ },
153
+
154
+ // Event bus
155
+ on(event, fn) {
156
+ if (!_eventBus[event]) _eventBus[event] = [];
157
+ _eventBus[event].push(fn);
158
+ return () => {
159
+ _eventBus[event] = _eventBus[event].filter((f) => f !== fn);
160
+ };
161
+ },
162
+
163
+ // Request interceptors
164
+ interceptor(type, fn) {
165
+ if (_interceptors[type]) _interceptors[type].push(fn);
166
+ },
167
+
168
+ // Access global stores
169
+ get store() {
170
+ return _stores;
171
+ },
172
+
173
+ // Access router
174
+ get router() {
175
+ return _routerInstance;
176
+ },
177
+
178
+ // Utilities (for custom directives)
179
+ createContext,
180
+ evaluate,
181
+ findContext,
182
+ processTree,
183
+ resolve,
184
+
185
+ // Version
186
+ version: "1.0.0",
187
+ };
188
+
189
+ export default NoJS;
@@ -0,0 +1,60 @@
1
+ // ═══════════════════════════════════════════════════════════════════════
2
+ // DIRECTIVE REGISTRY & DOM PROCESSING
3
+ // ═══════════════════════════════════════════════════════════════════════
4
+
5
+ const _directives = new Map();
6
+
7
+ export function registerDirective(name, handler) {
8
+ _directives.set(name, {
9
+ priority: handler.priority ?? 50,
10
+ init: handler.init,
11
+ });
12
+ }
13
+
14
+ function _matchDirective(attrName) {
15
+ if (_directives.has(attrName))
16
+ return { directive: _directives.get(attrName), match: attrName };
17
+ // Pattern matches
18
+ const patterns = ["class-*", "on:*", "style-*", "bind-*"];
19
+ for (const p of patterns) {
20
+ const prefix = p.replace("*", "");
21
+ if (attrName.startsWith(prefix) && _directives.has(p)) {
22
+ return { directive: _directives.get(p), match: p };
23
+ }
24
+ }
25
+ return null;
26
+ }
27
+
28
+ export function processElement(el) {
29
+ if (el.__declared) return;
30
+ el.__declared = true;
31
+
32
+ const matched = [];
33
+ for (const attr of [...el.attributes]) {
34
+ const m = _matchDirective(attr.name);
35
+ if (m) {
36
+ matched.push({
37
+ name: attr.name,
38
+ value: attr.value,
39
+ priority: m.directive.priority,
40
+ init: m.directive.init,
41
+ });
42
+ }
43
+ }
44
+
45
+ matched.sort((a, b) => a.priority - b.priority);
46
+ for (const m of matched) {
47
+ m.init(el, m.name, m.value);
48
+ }
49
+ }
50
+
51
+ export function processTree(root) {
52
+ if (!root) return;
53
+ if (root.nodeType === 1 && !root.__declared) processElement(root);
54
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
55
+ while (walker.nextNode()) {
56
+ const node = walker.currentNode;
57
+ if (node.tagName === "TEMPLATE" || node.tagName === "SCRIPT") continue;
58
+ if (!node.__declared) processElement(node);
59
+ }
60
+ }