Compare commits
2 commits
a8a7dc9445
...
b2056d4e2b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2056d4e2b | ||
|
|
d51d3f60f1 |
27 changed files with 3764 additions and 334 deletions
10
.gitignore
vendored
10
.gitignore
vendored
|
|
@ -3,3 +3,13 @@ node_modules/
|
||||||
dist/
|
dist/
|
||||||
.vite/
|
.vite/
|
||||||
coverage/
|
coverage/
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
dev/
|
||||||
|
min/
|
||||||
|
|
||||||
|
# Generated test files
|
||||||
|
test/extracted/
|
||||||
|
|
||||||
|
# Temporary test files
|
||||||
|
test-build.html
|
||||||
|
|
|
||||||
312
min/Ox.js
312
min/Ox.js
File diff suppressed because one or more lines are too long
11
package-lock.json
generated
11
package-lock.json
generated
|
|
@ -17,6 +17,7 @@
|
||||||
"postcss-import": "^16.0.0",
|
"postcss-import": "^16.0.0",
|
||||||
"postcss-nesting": "^12.0.2",
|
"postcss-nesting": "^12.0.2",
|
||||||
"rollup": "^4.9.5",
|
"rollup": "^4.9.5",
|
||||||
|
"terser": "^5.26.0",
|
||||||
"vite": "^5.0.11",
|
"vite": "^5.0.11",
|
||||||
"vitest": "^1.2.1"
|
"vitest": "^1.2.1"
|
||||||
}
|
}
|
||||||
|
|
@ -2241,7 +2242,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
|
||||||
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
|
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/gen-mapping": "^0.3.5",
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
"@jridgewell/trace-mapping": "^0.3.25"
|
"@jridgewell/trace-mapping": "^0.3.25"
|
||||||
|
|
@ -2998,8 +2998,7 @@
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/cac": {
|
"node_modules/cac": {
|
||||||
"version": "6.7.14",
|
"version": "6.7.14",
|
||||||
|
|
@ -3107,8 +3106,7 @@
|
||||||
"version": "2.20.3",
|
"version": "2.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
|
|
@ -4972,7 +4970,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
|
|
@ -4991,7 +4988,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer-from": "^1.0.0",
|
"buffer-from": "^1.0.0",
|
||||||
"source-map": "^0.6.0"
|
"source-map": "^0.6.0"
|
||||||
|
|
@ -5182,7 +5178,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz",
|
||||||
"integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==",
|
"integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/source-map": "^0.3.3",
|
"@jridgewell/source-map": "^0.3.3",
|
||||||
"acorn": "^8.15.0",
|
"acorn": "^8.15.0",
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "node scripts/build.js",
|
||||||
|
"build:vite": "vite build --config vite.config.build.js",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:run": "vitest run",
|
"test:run": "vitest run",
|
||||||
"extract-tests": "node scripts/extract-tests.js",
|
"extract-tests": "node scripts/extract-tests.js",
|
||||||
|
|
@ -45,6 +46,7 @@
|
||||||
"postcss-import": "^16.0.0",
|
"postcss-import": "^16.0.0",
|
||||||
"postcss-nesting": "^12.0.2",
|
"postcss-nesting": "^12.0.2",
|
||||||
"rollup": "^4.9.5",
|
"rollup": "^4.9.5",
|
||||||
|
"terser": "^5.26.0",
|
||||||
"vite": "^5.0.11",
|
"vite": "^5.0.11",
|
||||||
"vitest": "^1.2.1"
|
"vitest": "^1.2.1"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
105
scripts/build.js
Normal file
105
scripts/build.js
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build script for OxJS
|
||||||
|
* Generates ESM, UMD, and minified builds
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { build } = require('vite');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { minify } = require('terser');
|
||||||
|
|
||||||
|
async function buildOx() {
|
||||||
|
console.log('Building OxJS...\n');
|
||||||
|
|
||||||
|
// Step 1: Build ESM and UMD formats using Vite
|
||||||
|
console.log('1. Building ES modules and UMD...');
|
||||||
|
await build({
|
||||||
|
configFile: path.resolve(__dirname, '../vite.config.build.js')
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 2: Create minified version for script tag usage (min/Ox.js)
|
||||||
|
console.log('\n2. Creating minified build...');
|
||||||
|
|
||||||
|
// Read the UMD build
|
||||||
|
const umdPath = path.resolve(__dirname, '../dist/ox.umd.js');
|
||||||
|
const umdCode = fs.readFileSync(umdPath, 'utf-8');
|
||||||
|
|
||||||
|
// Minify with Terser
|
||||||
|
const minified = await minify(umdCode, {
|
||||||
|
compress: {
|
||||||
|
drop_console: false, // Keep console for debugging
|
||||||
|
drop_debugger: true,
|
||||||
|
pure_funcs: ['console.log']
|
||||||
|
},
|
||||||
|
mangle: {
|
||||||
|
reserved: ['Ox'] // Don't mangle the main Ox object
|
||||||
|
},
|
||||||
|
format: {
|
||||||
|
comments: false,
|
||||||
|
preamble: '/* OxJS v0.2.0 | (c) 2024 0x2620 | MIT License | oxjs.org */'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure min directory exists
|
||||||
|
const minDir = path.resolve(__dirname, '../min');
|
||||||
|
if (!fs.existsSync(minDir)) {
|
||||||
|
fs.mkdirSync(minDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write minified file
|
||||||
|
fs.writeFileSync(path.join(minDir, 'Ox.js'), minified.code);
|
||||||
|
|
||||||
|
// Step 3: Copy the minified file to be compatible with old path structure
|
||||||
|
console.log('\n3. Creating backward compatible structure...');
|
||||||
|
|
||||||
|
// Create dev symlink if it doesn't exist
|
||||||
|
const devPath = path.resolve(__dirname, '../dev');
|
||||||
|
if (!fs.existsSync(devPath)) {
|
||||||
|
fs.symlinkSync('source', devPath, 'dir');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Generate build info
|
||||||
|
const buildInfo = {
|
||||||
|
version: '0.2.0',
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
files: {
|
||||||
|
'dist/ox.esm.js': getFileSize('../dist/ox.esm.js'),
|
||||||
|
'dist/ox.umd.js': getFileSize('../dist/ox.umd.js'),
|
||||||
|
'min/Ox.js': getFileSize('../min/Ox.js')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.resolve(__dirname, '../dist/build-info.json'),
|
||||||
|
JSON.stringify(buildInfo, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('\n✅ Build complete!\n');
|
||||||
|
console.log('Generated files:');
|
||||||
|
console.log(` dist/ox.esm.js (${buildInfo.files['dist/ox.esm.js']})`);
|
||||||
|
console.log(` dist/ox.umd.js (${buildInfo.files['dist/ox.umd.js']})`);
|
||||||
|
console.log(` min/Ox.js (${buildInfo.files['min/Ox.js']})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileSize(relativePath) {
|
||||||
|
const filePath = path.resolve(__dirname, relativePath);
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
const stats = fs.statSync(filePath);
|
||||||
|
return formatBytes(stats.size);
|
||||||
|
}
|
||||||
|
return 'N/A';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBytes(bytes) {
|
||||||
|
if (bytes < 1024) return bytes + ' B';
|
||||||
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||||
|
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run build
|
||||||
|
buildOx().catch(error => {
|
||||||
|
console.error('Build failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
45
scripts/debug-extract.js
Normal file
45
scripts/debug-extract.js
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Test with a known file that has tests
|
||||||
|
const testFile = path.join(__dirname, '../source/Ox/js/Array.js');
|
||||||
|
const source = fs.readFileSync(testFile, 'utf-8');
|
||||||
|
|
||||||
|
// Regular expressions from Ox.doc
|
||||||
|
const re = {
|
||||||
|
multiline: /\/\*@([\w\W]+?)@?\*\//g,
|
||||||
|
singleline: /\/\/@\s*(.*?)\s*$/gm,
|
||||||
|
test: /^\s*>\s+(.+)$/,
|
||||||
|
expected: /^\s*([^>].*)$/,
|
||||||
|
item: /^(.+?)\s+<(.+?)>\s+(.+?)$/,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find all multiline comments
|
||||||
|
let match;
|
||||||
|
let found = 0;
|
||||||
|
console.log('Searching for /*@ comments in Array.js...\n');
|
||||||
|
|
||||||
|
while ((match = re.multiline.exec(source)) !== null) {
|
||||||
|
found++;
|
||||||
|
const content = match[1];
|
||||||
|
const lines = content.split('\n');
|
||||||
|
const firstLine = lines[0].trim();
|
||||||
|
|
||||||
|
console.log(`Found comment #${found}:`);
|
||||||
|
console.log('First line:', firstLine);
|
||||||
|
|
||||||
|
// Look for tests
|
||||||
|
let testCount = 0;
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.match(re.test)) {
|
||||||
|
testCount++;
|
||||||
|
console.log(' Test:', line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(` Total tests in this block: ${testCount}`);
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nTotal doc blocks found: ${found}`);
|
||||||
|
|
@ -47,10 +47,26 @@ function parseDocComments(source, filename) {
|
||||||
*/
|
*/
|
||||||
function parseDocContent(content, filename) {
|
function parseDocContent(content, filename) {
|
||||||
const lines = content.split('\n');
|
const lines = content.split('\n');
|
||||||
const firstLine = lines[0].trim();
|
|
||||||
const itemMatch = firstLine.match(re.item);
|
|
||||||
|
|
||||||
if (!itemMatch) return null;
|
// Find the first non-empty line that matches the item pattern
|
||||||
|
let itemMatch = null;
|
||||||
|
let itemName = 'Unknown';
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (trimmed) {
|
||||||
|
itemMatch = trimmed.match(re.item);
|
||||||
|
if (itemMatch) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!itemMatch) {
|
||||||
|
// If no item match, still try to extract tests with a generic name
|
||||||
|
// This handles cases where tests are in script blocks or without proper headers
|
||||||
|
itemMatch = ['', filename.replace(/.*\//, '').replace('.js', ''), 'tests', ''];
|
||||||
|
}
|
||||||
|
|
||||||
const doc = {
|
const doc = {
|
||||||
name: itemMatch[1],
|
name: itemMatch[1],
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { isArray, isBoolean, isEqual, isFunction, isNumber, isObject, isString, isUndefined, typeOf } from './Type.js';
|
import { isArray, isBoolean, isEqual, isFunction, isNumber, isObject, isString, isUndefined, typeOf } from './Type.js';
|
||||||
import { extend, filter, forEach, len, map, some } from './Collection.js';
|
import { extend, filter, forEach, len, map, slice, some, max, min } from './Collection.js';
|
||||||
import { clone, getset, isEmpty, makeObject } from './Object.js';
|
import { clone, contains, getset, isEmpty, makeObject, values, keyOf } from './Object.js';
|
||||||
import { cache, identity } from './Function.js';
|
import { cache, identity } from './Function.js';
|
||||||
|
import { loop } from './Core.js';
|
||||||
import { MAX_LATITUDE } from './Constants.js';
|
import { MAX_LATITUDE } from './Constants.js';
|
||||||
import { deg, mod } from './Math.js';
|
import { deg, mod, random } from './Math.js';
|
||||||
|
import { char, splice, clean, repeat } from './String.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turns an array into a list API
|
* Turns an array into a list API
|
||||||
|
|
@ -382,6 +384,90 @@ function getSortValues(array) {
|
||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.last <f> Gets or sets the last element of an array
|
||||||
|
Unlike `arrayWithALongName[arrayWithALongName.length - 1]`,
|
||||||
|
`Ox.last(arrayWithALongName)` is short.
|
||||||
|
<script>
|
||||||
|
Ox.test.array = [1, 2, 3];
|
||||||
|
</script>
|
||||||
|
> Ox.last(Ox.test.array)
|
||||||
|
3
|
||||||
|
> Ox.last(Ox.test.array, 4)
|
||||||
|
[1, 2, 4]
|
||||||
|
> Ox.test.array
|
||||||
|
[1, 2, 4]
|
||||||
|
> Ox.last('123')
|
||||||
|
'3'
|
||||||
|
@*/
|
||||||
|
export function last(array, value) {
|
||||||
|
var ret;
|
||||||
|
if (arguments.length == 1) {
|
||||||
|
ret = array[array.length - 1];
|
||||||
|
} else {
|
||||||
|
array[array.length - 1] = value;
|
||||||
|
ret = array;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.makeArray <f> Wraps any non-array in an array.
|
||||||
|
(value) -> <a> Array
|
||||||
|
value <*> Any value
|
||||||
|
> Ox.makeArray('foo')
|
||||||
|
['foo']
|
||||||
|
> Ox.makeArray(['foo'])
|
||||||
|
['foo']
|
||||||
|
@*/
|
||||||
|
// FIXME: rename to toArray
|
||||||
|
export function makeArray(value) {
|
||||||
|
var ret, type = typeOf(value);
|
||||||
|
if (type == 'arguments' || type == 'nodelist') {
|
||||||
|
ret = slice(value);
|
||||||
|
} else if (type == 'array') {
|
||||||
|
ret = value;
|
||||||
|
} else {
|
||||||
|
ret = [value];
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.range <f> Python-style range
|
||||||
|
(stop) -> <[n]> range
|
||||||
|
Returns an array of integers from `0` (inclusive) to `stop` (exclusive).
|
||||||
|
(start, stop) -> <[n]> range
|
||||||
|
Returns an array of integers from `start` (inclusive) to `stop`
|
||||||
|
(exclusive).
|
||||||
|
(start, stop, step) -> <[n]> range
|
||||||
|
Returns an array of numbers from `start` (inclusive) to `stop`
|
||||||
|
(exclusive), incrementing by `step`.
|
||||||
|
start <n> Start value
|
||||||
|
stop <n> Stop value
|
||||||
|
step <n> Step value
|
||||||
|
> Ox.range(3)
|
||||||
|
[0, 1, 2]
|
||||||
|
> Ox.range(1, 4)
|
||||||
|
[1, 2, 3]
|
||||||
|
> Ox.range(3, 0)
|
||||||
|
[3, 2, 1]
|
||||||
|
> Ox.range(1, 2, 0.5)
|
||||||
|
[1, 1.5]
|
||||||
|
> Ox.range(-1, -2, -0.5)
|
||||||
|
[-1, -1.5]
|
||||||
|
@*/
|
||||||
|
export function range() {
|
||||||
|
var array = [];
|
||||||
|
loop.apply(null, slice(arguments).concat(function(index) {
|
||||||
|
array.push(index);
|
||||||
|
}));
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-export functions from other modules to maintain original hierarchy
|
||||||
|
export { slice, max, min, forEach, len, map, filter, values, keyOf, isEmpty, contains, random, char, splice, clean, repeat, loop };
|
||||||
|
|
||||||
// Export all functions
|
// Export all functions
|
||||||
export default {
|
export default {
|
||||||
api,
|
api,
|
||||||
|
|
@ -389,5 +475,26 @@ export default {
|
||||||
count,
|
count,
|
||||||
sort,
|
sort,
|
||||||
unique,
|
unique,
|
||||||
zip
|
zip,
|
||||||
|
last,
|
||||||
|
makeArray,
|
||||||
|
range,
|
||||||
|
// Re-exported functions
|
||||||
|
slice,
|
||||||
|
max,
|
||||||
|
min,
|
||||||
|
forEach,
|
||||||
|
len,
|
||||||
|
map,
|
||||||
|
filter,
|
||||||
|
values,
|
||||||
|
keyOf,
|
||||||
|
isEmpty,
|
||||||
|
contains,
|
||||||
|
random,
|
||||||
|
char,
|
||||||
|
splice,
|
||||||
|
clean,
|
||||||
|
repeat,
|
||||||
|
loop
|
||||||
};
|
};
|
||||||
284
src/ox/core/Async.js
Normal file
284
src/ox/core/Async.js
Normal file
|
|
@ -0,0 +1,284 @@
|
||||||
|
import { typeOf, isFunction } from './Type.js';
|
||||||
|
import { last, makeArray, slice, forEach, len, range } from './Array.js';
|
||||||
|
|
||||||
|
function asyncMap(forEachFn, collection, iterator, that, callback) {
|
||||||
|
var type = typeOf(collection),
|
||||||
|
results = type == 'object' ? {} : [];
|
||||||
|
callback = last(arguments);
|
||||||
|
that = arguments.length == 5 ? that : null;
|
||||||
|
forEachFn(collection, function(value, key, collection, callback) {
|
||||||
|
iterator(value, key, collection, function(value) {
|
||||||
|
results[key] = value;
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
}, that, function() {
|
||||||
|
callback(type == 'string' ? results.join('') : results);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function asyncMapFn(array, iterator, that, callback) {
|
||||||
|
array = makeArray(array);
|
||||||
|
callback = last(arguments);
|
||||||
|
that = arguments.length == 4 ? that : null;
|
||||||
|
if (array.some(Array.isArray)) {
|
||||||
|
serialMap(array, function(value, key, array, callback) {
|
||||||
|
parallelMap(makeArray(value), iterator, callback);
|
||||||
|
}, callback);
|
||||||
|
} else {
|
||||||
|
parallelMap(array, iterator, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.nonblockingForEach <f> Non-blocking `forEach` with synchronous iterator
|
||||||
|
(col, iterator[, that], callback[, ms]) -> <u> undefined
|
||||||
|
collection <a|o|s> Collection
|
||||||
|
iterator <f> Iterator function
|
||||||
|
value <*> Value
|
||||||
|
key <n|s> Key
|
||||||
|
collection <a|o|s> The collection
|
||||||
|
that <o> The iterator's `this` binding
|
||||||
|
callback <f> Callback function
|
||||||
|
ms <n> Number of milliseconds after which to insert a `setTimeout` call
|
||||||
|
@*/
|
||||||
|
export function nonblockingForEach(collection, iterator, that, callback, ms) {
|
||||||
|
var i = 0, keys, lastArg = last(arguments),
|
||||||
|
n, time, type = typeOf(collection);
|
||||||
|
callback = isFunction(lastArg) ? lastArg : arguments[arguments.length - 2];
|
||||||
|
collection = type == 'array' || type == 'object'
|
||||||
|
? collection : slice(collection);
|
||||||
|
keys = type == 'object'
|
||||||
|
? Object.keys(collection) : range(collection.length);
|
||||||
|
ms = ms || 1000;
|
||||||
|
n = len(collection);
|
||||||
|
that = arguments.length == 5 || (
|
||||||
|
arguments.length == 4 && isFunction(lastArg)
|
||||||
|
) ? that : null;
|
||||||
|
time = +new Date();
|
||||||
|
iterate();
|
||||||
|
function iterate() {
|
||||||
|
forEach(keys.slice(i), function(key) {
|
||||||
|
if (key in collection) {
|
||||||
|
if (iterator.call(
|
||||||
|
that, collection[key], key, collection
|
||||||
|
) === false) {
|
||||||
|
i = n;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
if (+new Date() >= time + ms) {
|
||||||
|
return false; // break
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (i < n) {
|
||||||
|
setTimeout(function() {
|
||||||
|
time = +new Date();
|
||||||
|
iterate();
|
||||||
|
}, 1);
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.nonblockingMap <f> Non-blocking `map` with synchronous iterator
|
||||||
|
(collection, iterator[, that], callback[, ms]) -> <u> undefined
|
||||||
|
collection <a|o|s> Collection
|
||||||
|
iterator <f> Iterator function
|
||||||
|
that <o> The iterator's `this` binding
|
||||||
|
callback <f> Callback function
|
||||||
|
ms <n> Number of milliseconds after which to insert a `setTimeout` call
|
||||||
|
<script>
|
||||||
|
// var time = +new Date();
|
||||||
|
// Ox.nonblockingMap(
|
||||||
|
// Ox.range(1000000),
|
||||||
|
// function (value, index, array) {
|
||||||
|
// return +new Date() - time;
|
||||||
|
// },
|
||||||
|
// function(results) {
|
||||||
|
// Ox.print(results.length);
|
||||||
|
// },
|
||||||
|
// 1000
|
||||||
|
// );
|
||||||
|
</script>
|
||||||
|
> Ox.nonblockingMap(Ox.range(100000), Ox.identity, function(r) { Ox.test(r.length, 100000); })
|
||||||
|
undefined
|
||||||
|
@*/
|
||||||
|
export function nonblockingMap(collection, iterator, that, callback, ms) {
|
||||||
|
var lastArg = last(arguments),
|
||||||
|
type = typeOf(collection),
|
||||||
|
results = type == 'object' ? {} : [];
|
||||||
|
callback = isFunction(lastArg) ? lastArg : arguments[arguments.length - 2];
|
||||||
|
that = arguments.length == 5 || (
|
||||||
|
arguments.length == 4 && isFunction(lastArg)
|
||||||
|
) ? that : null;
|
||||||
|
nonblockingForEach(collection, function(value, key, collection) {
|
||||||
|
results[key] = iterator.call(that, value, key, collection);
|
||||||
|
}, function() {
|
||||||
|
callback(type == 'string' ? results.join('') : results);
|
||||||
|
}, ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.parallelForEach <f> `forEach` with asynchronous iterator, running in parallel
|
||||||
|
(collection, iterator[, that], callback) -> <u> undefined
|
||||||
|
collection <a|o|s> Collection
|
||||||
|
iterator <f> Iterator function
|
||||||
|
value <*> Value
|
||||||
|
key <n|s> Key
|
||||||
|
collection <a|o|s> The collection
|
||||||
|
callback <f> Callback function
|
||||||
|
that <o> The iterator's this binding
|
||||||
|
callback <f> Callback function
|
||||||
|
<script>
|
||||||
|
Ox.test.pfeNumber = 0;
|
||||||
|
Ox.test.pfeIterator = function(value, index, array, callback) {
|
||||||
|
if (index < 5) {
|
||||||
|
Ox.test.pfeNumber++;
|
||||||
|
}
|
||||||
|
setTimeout(callback);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
> Ox.parallelForEach(Ox.range(10), Ox.test.pfeIterator, function() { Ox.test(Ox.test.pfeNumber, 5); })
|
||||||
|
undefined
|
||||||
|
@*/
|
||||||
|
export function parallelForEach(collection, iterator, that, callback) {
|
||||||
|
var i = 0, n, type = typeOf(collection);
|
||||||
|
callback = callback || (arguments.length == 3 ? arguments[2] : function() {});
|
||||||
|
collection = type == 'array' || type == 'object'
|
||||||
|
? collection : slice(collection);
|
||||||
|
n = len(collection);
|
||||||
|
that = arguments.length == 4 ? that : null;
|
||||||
|
forEach(collection, function(value, key, collection) {
|
||||||
|
iterator.call(that, value, key, collection, function() {
|
||||||
|
++i == n && callback();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.parallelMap <f> Parallel `map` with asynchronous iterator
|
||||||
|
(collection, iterator[, that], callback) -> <u> undefined
|
||||||
|
collection <a|o|s> Collection
|
||||||
|
iterator <f> Iterator function
|
||||||
|
value <*> Value
|
||||||
|
key <n|s> Key
|
||||||
|
collection <a|o|s> The collection
|
||||||
|
callback <f> Callback function
|
||||||
|
that <o> The iterator's this binding
|
||||||
|
callback <f> Callback function
|
||||||
|
results <a|o|s> Results
|
||||||
|
<script>
|
||||||
|
// var time = +new Date();
|
||||||
|
// Ox.parallelMap(
|
||||||
|
// Ox.range(10),
|
||||||
|
// function (value, index, array, callback) {
|
||||||
|
// setTimeout(function() {
|
||||||
|
// callback(+new Date() - time);
|
||||||
|
// }, Ox.random(1000));
|
||||||
|
// },
|
||||||
|
// function(results) {
|
||||||
|
// Ox.print(results);
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
Ox.test.pmIterator = function(value, index, array, callback) {
|
||||||
|
setTimeout(callback(value - index));
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
> Ox.parallelMap(Ox.range(10), Ox.test.pmIterator, function(r) { Ox.test(Ox.sum(r), 0); })
|
||||||
|
undefined
|
||||||
|
@*/
|
||||||
|
export function parallelMap() {
|
||||||
|
asyncMap.apply(null, [parallelForEach].concat(slice(arguments)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.serialForEach <f> `forEach` with asynchronous iterator, run serially
|
||||||
|
(collection, iterator[, that], callback) -> <u> undefined
|
||||||
|
collection <a|o|s> Collection
|
||||||
|
iterator <f> Iterator function
|
||||||
|
value <*> Value
|
||||||
|
key <n|s> Key
|
||||||
|
collection <a|o|s> The collection
|
||||||
|
callback <f> Callback function
|
||||||
|
that <o> The iterator's this binding
|
||||||
|
callback <f> Callback function
|
||||||
|
<script>
|
||||||
|
Ox.test.sfeNumber = 0;
|
||||||
|
Ox.test.sfeIterator = function(value, index, array, callback) {
|
||||||
|
Ox.test.sfeNumber++;
|
||||||
|
setTimeout(function() {
|
||||||
|
callback(index < 4);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
> Ox.serialForEach(Ox.range(10), Ox.test.sfeIterator, function() { Ox.test(Ox.test.sfeNumber, 5); })
|
||||||
|
undefined
|
||||||
|
@*/
|
||||||
|
export function serialForEach(collection, iterator, that, callback) {
|
||||||
|
var i = 0, keys, n, type = typeOf(collection);
|
||||||
|
callback = callback || (arguments.length == 3 ? arguments[2] : function() {});
|
||||||
|
collection = type == 'array' || type == 'object'
|
||||||
|
? collection : slice(collection);
|
||||||
|
keys = type == 'object'
|
||||||
|
? Object.keys(collection) : range(collection.length);
|
||||||
|
n = len(collection);
|
||||||
|
that = arguments.length == 4 ? that : null;
|
||||||
|
iterate();
|
||||||
|
function iterate(value) {
|
||||||
|
if (value !== false) {
|
||||||
|
if (keys[i] in collection) {
|
||||||
|
iterator.call(
|
||||||
|
that,
|
||||||
|
collection[keys[i]],
|
||||||
|
keys[i],
|
||||||
|
collection,
|
||||||
|
++i < n ? iterate : callback
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
++i < n ? iterate() : callback();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.serialMap <f> Serial `map` with asynchronous iterator
|
||||||
|
(collection, iterator[, that], callback) -> <u> undefined
|
||||||
|
collection <a|o|s> Collection
|
||||||
|
iterator <f> Iterator function
|
||||||
|
value <*> Value
|
||||||
|
key <n|s> Key
|
||||||
|
collection <a|o|s> The collection
|
||||||
|
callback <f> Callback function
|
||||||
|
that <o> The iterator's this binding
|
||||||
|
callback <f> Callback function
|
||||||
|
results <a|o|s> Results
|
||||||
|
<script>
|
||||||
|
// var time = +new Date();
|
||||||
|
// Ox.serialMap(
|
||||||
|
// Ox.range(10),
|
||||||
|
// function (value, index, array, callback) {
|
||||||
|
// setTimeout(function() {
|
||||||
|
// callback(+new Date() - time);
|
||||||
|
// }, Ox.random(1000));
|
||||||
|
// },
|
||||||
|
// function(results) {
|
||||||
|
// Ox.print(results);
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
Ox.test.smIterator = function(value, index, array, callback) {
|
||||||
|
setTimeout(callback(value - index));
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
> Ox.serialMap(Ox.range(10), Ox.test.smIterator, function(r) { Ox.test(Ox.sum(r), 0); })
|
||||||
|
undefined
|
||||||
|
@*/
|
||||||
|
export function serialMap(collection, iterator, that, callback) {
|
||||||
|
asyncMap.apply(null, [serialForEach].concat(slice(arguments)));
|
||||||
|
}
|
||||||
|
// FIXME: The above test with 10000 iterations blows the stack
|
||||||
|
|
@ -208,6 +208,104 @@ export function values(collection) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants
|
||||||
|
*/
|
||||||
|
const STACK_LENGTH = 50000;
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.slice <f> Alias for `Array.prototype.slice.call`
|
||||||
|
(collection[, start[, stop]]) -> <a> Array
|
||||||
|
collection <a|o|s> Array-like
|
||||||
|
start <n> Start position
|
||||||
|
stop <n> Stop position
|
||||||
|
> (function() { return Ox.slice(arguments); }(1, 2, 3))
|
||||||
|
[1, 2, 3]
|
||||||
|
> Ox.slice('foo', 0, 1);
|
||||||
|
['f']
|
||||||
|
> Ox.slice({0: 'f', 1: 'o', 2: 'o', length: 3}, -2)
|
||||||
|
['o', 'o']
|
||||||
|
@*/
|
||||||
|
// FIXME: remove toArray alias
|
||||||
|
export function slice(collection, start, stop) {
|
||||||
|
// IE8 can't apply slice to NodeLists, returns an empty array if undefined is
|
||||||
|
// passed as stop and returns an array of null values if a string is passed as
|
||||||
|
// value. Firefox 3.6 returns an array of undefined values if a string is passed
|
||||||
|
// as value.
|
||||||
|
|
||||||
|
// Try the simple approach first
|
||||||
|
try {
|
||||||
|
const result = Array.prototype.slice.call(collection, start, stop);
|
||||||
|
// Test for broken implementations
|
||||||
|
if (result.length === 0 && collection.length > 0) {
|
||||||
|
throw new Error('Broken slice implementation');
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
// Fallback for broken implementations
|
||||||
|
const args = stop === void 0 ? [start] : [start, stop];
|
||||||
|
const array = [];
|
||||||
|
let index, length;
|
||||||
|
|
||||||
|
if (typeOf(collection) === 'string') {
|
||||||
|
collection = collection.split('');
|
||||||
|
}
|
||||||
|
|
||||||
|
length = collection.length;
|
||||||
|
for (index = 0; index < length; index++) {
|
||||||
|
array[index] = collection[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.prototype.slice.apply(array, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.max <f> Returns the maximum value of a collection
|
||||||
|
> Ox.max([1, 2, 3])
|
||||||
|
3
|
||||||
|
> Ox.max({a: 1, b: 2, c: 3})
|
||||||
|
3
|
||||||
|
> Ox.max('123')
|
||||||
|
3
|
||||||
|
> Ox.max([])
|
||||||
|
-Infinity
|
||||||
|
@*/
|
||||||
|
export function max(collection) {
|
||||||
|
var ret, collectionValues = values(collection);
|
||||||
|
if (collectionValues.length < STACK_LENGTH) {
|
||||||
|
ret = Math.max.apply(null, collectionValues);
|
||||||
|
} else {
|
||||||
|
ret = collectionValues.reduce(function(previousValue, currentValue) {
|
||||||
|
return Math.max(previousValue, currentValue);
|
||||||
|
}, -Infinity);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.min <f> Returns the minimum value of a collection
|
||||||
|
> Ox.min([1, 2, 3])
|
||||||
|
1
|
||||||
|
> Ox.min({a: 1, b: 2, c: 3})
|
||||||
|
1
|
||||||
|
> Ox.min('123')
|
||||||
|
1
|
||||||
|
> Ox.min([])
|
||||||
|
Infinity
|
||||||
|
@*/
|
||||||
|
export function min(collection) {
|
||||||
|
var ret, collectionValues = values(collection);
|
||||||
|
if (collectionValues.length < STACK_LENGTH) {
|
||||||
|
ret = Math.min.apply(null, collectionValues);
|
||||||
|
} else {
|
||||||
|
ret = collectionValues.reduce(function(previousValue, currentValue) {
|
||||||
|
return Math.min(previousValue, currentValue);
|
||||||
|
}, Infinity);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
// Export all functions
|
// Export all functions
|
||||||
export default {
|
export default {
|
||||||
forEach,
|
forEach,
|
||||||
|
|
@ -219,5 +317,8 @@ export default {
|
||||||
every,
|
every,
|
||||||
some,
|
some,
|
||||||
keys,
|
keys,
|
||||||
values
|
values,
|
||||||
|
slice,
|
||||||
|
max,
|
||||||
|
min
|
||||||
};
|
};
|
||||||
125
src/ox/core/Color.js
Normal file
125
src/ox/core/Color.js
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
import { slice, max, min, range } from './Array.js';
|
||||||
|
import { clone } from './Object.js';
|
||||||
|
import { pad } from './String.js';
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.hsl <f> Takes RGB values and returns HSL values
|
||||||
|
(rgb) <[n]> HSL values
|
||||||
|
(r, g, b) <[n]> HSL values
|
||||||
|
rgb <[n]> RGB values
|
||||||
|
r <n> red
|
||||||
|
g <n> green
|
||||||
|
b <n> blue
|
||||||
|
> Ox.hsl([0, 0, 0])
|
||||||
|
[0, 0, 0]
|
||||||
|
> Ox.hsl([255, 255, 255])
|
||||||
|
[0, 0, 1]
|
||||||
|
> Ox.hsl(0, 255, 0)
|
||||||
|
[120, 1, 0.5]
|
||||||
|
@*/
|
||||||
|
export function hsl(rgb) {
|
||||||
|
var hsl = [0, 0, 0], maxVal, minVal;
|
||||||
|
if (arguments.length == 3) {
|
||||||
|
rgb = slice(arguments);
|
||||||
|
}
|
||||||
|
rgb = clone(rgb).map(function(value) {
|
||||||
|
return value / 255;
|
||||||
|
});
|
||||||
|
maxVal = max(rgb);
|
||||||
|
minVal = min(rgb);
|
||||||
|
hsl[2] = 0.5 * (maxVal + minVal);
|
||||||
|
if (maxVal == minVal) {
|
||||||
|
hsl[0] = 0;
|
||||||
|
hsl[1] = 0;
|
||||||
|
} else {
|
||||||
|
if (maxVal == rgb[0]) {
|
||||||
|
hsl[0] = (60 * (rgb[1] - rgb[2]) / (maxVal - minVal) + 360) % 360;
|
||||||
|
} else if (maxVal == rgb[1]) {
|
||||||
|
hsl[0] = 60 * (rgb[2] - rgb[0]) / (maxVal - minVal) + 120;
|
||||||
|
} else if (maxVal == rgb[2]) {
|
||||||
|
hsl[0] = 60 * (rgb[0] - rgb[1]) / (maxVal - minVal) + 240;
|
||||||
|
}
|
||||||
|
if (hsl[2] <= 0.5) {
|
||||||
|
hsl[1] = (maxVal - minVal) / (2 * hsl[2]);
|
||||||
|
} else {
|
||||||
|
hsl[1] = (maxVal - minVal) / (2 - 2 * hsl[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hsl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.rgb <f> Takes HSL values and returns RGB values
|
||||||
|
(hsl) <[n]> RGB values
|
||||||
|
(h, s, l) <[n]> RGB values
|
||||||
|
hsl <[n]> HSL values
|
||||||
|
h <n> hue
|
||||||
|
s <n> saturation
|
||||||
|
l <n> lightness
|
||||||
|
> Ox.rgb([0, 0, 0])
|
||||||
|
[0, 0, 0]
|
||||||
|
> Ox.rgb([0, 0, 1])
|
||||||
|
[255, 255, 255]
|
||||||
|
> Ox.rgb(120, 1, 0.5)
|
||||||
|
[0, 255, 0]
|
||||||
|
@*/
|
||||||
|
export function rgb(hsl) {
|
||||||
|
var rgb = [0, 0, 0], v1, v2, v3;
|
||||||
|
if (arguments.length == 3) {
|
||||||
|
hsl = slice(arguments);
|
||||||
|
}
|
||||||
|
hsl = clone(hsl);
|
||||||
|
hsl[0] /= 360;
|
||||||
|
if (hsl[1] == 0) {
|
||||||
|
rgb = [hsl[2], hsl[2], hsl[2]];
|
||||||
|
} else {
|
||||||
|
if (hsl[2] < 0.5) {
|
||||||
|
v2 = hsl[2] * (1 + hsl[1]);
|
||||||
|
} else {
|
||||||
|
v2 = hsl[1] + hsl[2] - (hsl[1] * hsl[2]);
|
||||||
|
}
|
||||||
|
v1 = 2 * hsl[2] - v2;
|
||||||
|
rgb.forEach(function(v, i) {
|
||||||
|
v3 = hsl[0] + (1 - i) * 1/3;
|
||||||
|
if (v3 < 0) {
|
||||||
|
v3++;
|
||||||
|
} else if (v3 > 1) {
|
||||||
|
v3--;
|
||||||
|
}
|
||||||
|
if (v3 < 1/6) {
|
||||||
|
rgb[i] = v1 + ((v2 - v1) * 6 * v3);
|
||||||
|
} else if (v3 < 0.5) {
|
||||||
|
rgb[i] = v2;
|
||||||
|
} else if (v3 < 2/3) {
|
||||||
|
rgb[i] = v1 + ((v2 - v1) * 6 * (2/3 - v3));
|
||||||
|
} else {
|
||||||
|
rgb[i] = v1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return rgb.map(function(value) {
|
||||||
|
return Math.round(value * 255);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.toHex <f> Format RGB array as hex value
|
||||||
|
> Ox.toHex([192, 128, 64])
|
||||||
|
'C08040'
|
||||||
|
@*/
|
||||||
|
export function toHex(rgb) {
|
||||||
|
return rgb.map(function(value) {
|
||||||
|
return pad(value.toString(16).toUpperCase(), 'left', 2, '0');
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.toRGB <f> Format hex value as RGB array
|
||||||
|
> Ox.toRGB('C08040')
|
||||||
|
[192, 128, 64]
|
||||||
|
@*/
|
||||||
|
export function toRGB(hex) {
|
||||||
|
return range(3).map(function(index) {
|
||||||
|
return parseInt(hex.substr(index * 2, 2), 16);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,10 @@ export const EARTH_RADIUS = 6371000; // in meters
|
||||||
export const MAX_LATITUDE = 85.05112878; // Web Mercator max latitude
|
export const MAX_LATITUDE = 85.05112878; // Web Mercator max latitude
|
||||||
export const MIN_LATITUDE = -85.05112878; // Web Mercator min latitude
|
export const MIN_LATITUDE = -85.05112878; // Web Mercator min latitude
|
||||||
|
|
||||||
|
// Base32 encoding constants
|
||||||
|
export const BASE_32_ALIASES = {'I': '1', 'L': '1', 'O': '0', 'U': 'V'};
|
||||||
|
export const BASE_32_DIGITS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
||||||
|
|
||||||
// Time constants
|
// Time constants
|
||||||
export const SECONDS_PER_MINUTE = 60;
|
export const SECONDS_PER_MINUTE = 60;
|
||||||
export const SECONDS_PER_HOUR = 3600;
|
export const SECONDS_PER_HOUR = 3600;
|
||||||
|
|
@ -254,6 +258,8 @@ export default {
|
||||||
EARTH_RADIUS,
|
EARTH_RADIUS,
|
||||||
MAX_LATITUDE,
|
MAX_LATITUDE,
|
||||||
MIN_LATITUDE,
|
MIN_LATITUDE,
|
||||||
|
BASE_32_ALIASES,
|
||||||
|
BASE_32_DIGITS,
|
||||||
SECONDS_PER_MINUTE,
|
SECONDS_PER_MINUTE,
|
||||||
SECONDS_PER_HOUR,
|
SECONDS_PER_HOUR,
|
||||||
SECONDS_PER_DAY,
|
SECONDS_PER_DAY,
|
||||||
|
|
|
||||||
452
src/ox/core/Date.js
Normal file
452
src/ox/core/Date.js
Normal file
|
|
@ -0,0 +1,452 @@
|
||||||
|
/**
|
||||||
|
* Date utilities - ES Module Version
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { isDate, isNumber, isString, isUndefined } from './Type.js';
|
||||||
|
import { mod } from './Math.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the name of the day of the week for a given date
|
||||||
|
*/
|
||||||
|
export function getDayName(date, utc) {
|
||||||
|
const names = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||||
|
return names[getDayOfWeek(date, utc)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the day of the week (0-6) for a given date
|
||||||
|
*/
|
||||||
|
export function getDayOfWeek(date, utc) {
|
||||||
|
date = makeDate(date);
|
||||||
|
return utc ? date.getUTCDay() : date.getDay();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the day of the year (1-366) for a given date
|
||||||
|
*/
|
||||||
|
export function getDayOfYear(date, utc) {
|
||||||
|
date = makeDate(date);
|
||||||
|
const startOfYear = new Date(Date.UTC(
|
||||||
|
getFullYear(date, utc),
|
||||||
|
0, 1
|
||||||
|
));
|
||||||
|
const diff = date - startOfYear;
|
||||||
|
return Math.floor(diff / 86400000) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of days in a month
|
||||||
|
*/
|
||||||
|
export function getDaysInMonth(year, month) {
|
||||||
|
return new Date(year, month + 1, 0).getDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of days in a year
|
||||||
|
*/
|
||||||
|
export function getDaysInYear(year) {
|
||||||
|
return isLeapYear(year) ? 366 : 365;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the first day of the week for a given date
|
||||||
|
*/
|
||||||
|
export function getFirstDayOfWeek(date, utc) {
|
||||||
|
date = makeDate(date);
|
||||||
|
const day = getDayOfWeek(date, utc);
|
||||||
|
return new Date(date.getTime() - day * 86400000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the full year from a date
|
||||||
|
*/
|
||||||
|
export function getFullYear(date, utc) {
|
||||||
|
date = makeDate(date);
|
||||||
|
return utc ? date.getUTCFullYear() : date.getFullYear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get hours from date
|
||||||
|
*/
|
||||||
|
export function getHours(date, utc) {
|
||||||
|
date = makeDate(date);
|
||||||
|
return utc ? date.getUTCHours() : date.getHours();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ISO date string (YYYY-MM-DD)
|
||||||
|
*/
|
||||||
|
export function getISODate(date, utc) {
|
||||||
|
return formatDate(date, '%Y-%m-%d', utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ISO week number
|
||||||
|
*/
|
||||||
|
export function getISOWeek(date, utc) {
|
||||||
|
date = makeDate(date);
|
||||||
|
const year = getFullYear(date, utc);
|
||||||
|
const firstThursday = getFirstThursday(year, utc);
|
||||||
|
const week = Math.floor((date - firstThursday) / 604800000) + 1;
|
||||||
|
|
||||||
|
if (week < 1) {
|
||||||
|
return getISOWeek(new Date(year - 1, 11, 31), utc);
|
||||||
|
} else if (week > 52) {
|
||||||
|
const nextFirstThursday = getFirstThursday(year + 1, utc);
|
||||||
|
if (date >= nextFirstThursday) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return week;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ISO year
|
||||||
|
*/
|
||||||
|
export function getISOYear(date, utc) {
|
||||||
|
date = makeDate(date);
|
||||||
|
const year = getFullYear(date, utc);
|
||||||
|
const week = getISOWeek(date, utc);
|
||||||
|
|
||||||
|
if (week === 1 && getMonth(date, utc) === 11) {
|
||||||
|
return year + 1;
|
||||||
|
} else if (week >= 52 && getMonth(date, utc) === 0) {
|
||||||
|
return year - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return year;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get minutes from date
|
||||||
|
*/
|
||||||
|
export function getMinutes(date, utc) {
|
||||||
|
date = makeDate(date);
|
||||||
|
return utc ? date.getUTCMinutes() : date.getMinutes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get month from date (0-11)
|
||||||
|
*/
|
||||||
|
export function getMonth(date, utc) {
|
||||||
|
date = makeDate(date);
|
||||||
|
return utc ? date.getUTCMonth() : date.getMonth();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get month name
|
||||||
|
*/
|
||||||
|
export function getMonthName(date, utc) {
|
||||||
|
const names = [
|
||||||
|
'January', 'February', 'March', 'April', 'May', 'June',
|
||||||
|
'July', 'August', 'September', 'October', 'November', 'December'
|
||||||
|
];
|
||||||
|
return names[getMonth(date, utc)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get seconds from date
|
||||||
|
*/
|
||||||
|
export function getSeconds(date, utc) {
|
||||||
|
date = makeDate(date);
|
||||||
|
return utc ? date.getUTCSeconds() : date.getSeconds();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get milliseconds from date
|
||||||
|
*/
|
||||||
|
export function getMilliseconds(date, utc) {
|
||||||
|
date = makeDate(date);
|
||||||
|
return utc ? date.getUTCMilliseconds() : date.getMilliseconds();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get timezone offset in minutes
|
||||||
|
*/
|
||||||
|
export function getTimezoneOffset(date) {
|
||||||
|
return makeDate(date).getTimezoneOffset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get timezone offset string (+HH:MM or -HH:MM)
|
||||||
|
*/
|
||||||
|
export function getTimezoneOffsetString(date) {
|
||||||
|
const offset = getTimezoneOffset(date);
|
||||||
|
const sign = offset <= 0 ? '+' : '-';
|
||||||
|
const hours = Math.floor(Math.abs(offset) / 60);
|
||||||
|
const minutes = Math.abs(offset) % 60;
|
||||||
|
return sign + pad(hours, 2) + ':' + pad(minutes, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Unix timestamp (seconds since epoch)
|
||||||
|
*/
|
||||||
|
export function getUnixTime(date) {
|
||||||
|
return Math.floor(makeDate(date).getTime() / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get week number (1-53)
|
||||||
|
*/
|
||||||
|
export function getWeek(date, utc) {
|
||||||
|
date = makeDate(date);
|
||||||
|
const firstDayOfYear = new Date(Date.UTC(
|
||||||
|
getFullYear(date, utc), 0, 1
|
||||||
|
));
|
||||||
|
const days = Math.floor((date - firstDayOfYear) / 86400000);
|
||||||
|
return Math.ceil((days + getDayOfWeek(firstDayOfYear, utc) + 1) / 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a year is a leap year
|
||||||
|
*/
|
||||||
|
export function isLeapYear(year) {
|
||||||
|
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a date is valid
|
||||||
|
*/
|
||||||
|
export function isValidDate(date) {
|
||||||
|
date = makeDate(date);
|
||||||
|
return !isNaN(date.getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a date object from various inputs
|
||||||
|
*/
|
||||||
|
export function makeDate(date) {
|
||||||
|
if (isDate(date)) {
|
||||||
|
return date;
|
||||||
|
} else if (isString(date)) {
|
||||||
|
return new Date(date);
|
||||||
|
} else if (isNumber(date)) {
|
||||||
|
return new Date(date);
|
||||||
|
} else if (isUndefined(date)) {
|
||||||
|
return new Date();
|
||||||
|
}
|
||||||
|
return new Date(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a date according to a format string
|
||||||
|
*/
|
||||||
|
export function formatDate(date, format, utc) {
|
||||||
|
date = makeDate(date);
|
||||||
|
format = format || '%Y-%m-%d %H:%M:%S';
|
||||||
|
|
||||||
|
const replacements = {
|
||||||
|
'%a': () => getDayName(date, utc).substr(0, 3),
|
||||||
|
'%A': () => getDayName(date, utc),
|
||||||
|
'%b': () => getMonthName(date, utc).substr(0, 3),
|
||||||
|
'%B': () => getMonthName(date, utc),
|
||||||
|
'%c': () => date.toLocaleString(),
|
||||||
|
'%d': () => pad(getDate(date, utc), 2),
|
||||||
|
'%e': () => pad(getDate(date, utc), 2, ' '),
|
||||||
|
'%H': () => pad(getHours(date, utc), 2),
|
||||||
|
'%I': () => pad(((getHours(date, utc) + 11) % 12) + 1, 2),
|
||||||
|
'%j': () => pad(getDayOfYear(date, utc), 3),
|
||||||
|
'%k': () => pad(getHours(date, utc), 2, ' '),
|
||||||
|
'%l': () => pad(((getHours(date, utc) + 11) % 12) + 1, 2, ' '),
|
||||||
|
'%m': () => pad(getMonth(date, utc) + 1, 2),
|
||||||
|
'%M': () => pad(getMinutes(date, utc), 2),
|
||||||
|
'%p': () => getHours(date, utc) < 12 ? 'AM' : 'PM',
|
||||||
|
'%S': () => pad(getSeconds(date, utc), 2),
|
||||||
|
'%u': () => getDayOfWeek(date, utc) || 7,
|
||||||
|
'%U': () => pad(getWeek(date, utc), 2),
|
||||||
|
'%V': () => pad(getISOWeek(date, utc), 2),
|
||||||
|
'%w': () => getDayOfWeek(date, utc),
|
||||||
|
'%W': () => pad(getWeek(date, utc), 2),
|
||||||
|
'%x': () => date.toLocaleDateString(),
|
||||||
|
'%X': () => date.toLocaleTimeString(),
|
||||||
|
'%y': () => pad(getFullYear(date, utc) % 100, 2),
|
||||||
|
'%Y': () => getFullYear(date, utc),
|
||||||
|
'%z': () => getTimezoneOffsetString(date),
|
||||||
|
'%Z': () => '', // Timezone abbreviation not easily available
|
||||||
|
'%%': () => '%'
|
||||||
|
};
|
||||||
|
|
||||||
|
return format.replace(/%[a-zA-Z%]/g, (match) => {
|
||||||
|
return replacements[match] ? replacements[match]() : match;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a date string
|
||||||
|
*/
|
||||||
|
export function parseDate(string, format, utc) {
|
||||||
|
// Basic implementation - can be enhanced
|
||||||
|
return new Date(string);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get date (day of month)
|
||||||
|
*/
|
||||||
|
export function getDate(date, utc) {
|
||||||
|
date = makeDate(date);
|
||||||
|
return utc ? date.getUTCDate() : date.getDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get day (alias for getDayOfWeek)
|
||||||
|
*/
|
||||||
|
export function getDay(date, utc) {
|
||||||
|
return getDayOfWeek(date, utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get ISO day (Monday=1, Sunday=7)
|
||||||
|
*/
|
||||||
|
export function getISODay(date, utc) {
|
||||||
|
const day = getDayOfWeek(date, utc);
|
||||||
|
return day === 0 ? 7 : day;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get day of the year (alias for getDayOfYear)
|
||||||
|
*/
|
||||||
|
export function getDayOfTheYear(date, utc) {
|
||||||
|
return getDayOfYear(date, utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get first day of the year
|
||||||
|
*/
|
||||||
|
export function getFirstDayOfTheYear(date, utc) {
|
||||||
|
date = makeDate(date);
|
||||||
|
const year = getFullYear(date, utc);
|
||||||
|
return new Date(Date.UTC(year, 0, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set date (day of month)
|
||||||
|
*/
|
||||||
|
export function setDate(date, day, utc) {
|
||||||
|
date = makeDate(date);
|
||||||
|
if (utc) {
|
||||||
|
date.setUTCDate(day);
|
||||||
|
} else {
|
||||||
|
date.setDate(day);
|
||||||
|
}
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set full year
|
||||||
|
*/
|
||||||
|
export function setFullYear(date, year, utc) {
|
||||||
|
date = makeDate(date);
|
||||||
|
if (utc) {
|
||||||
|
date.setUTCFullYear(year);
|
||||||
|
} else {
|
||||||
|
date.setFullYear(year);
|
||||||
|
}
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set month
|
||||||
|
*/
|
||||||
|
export function setMonth(date, month, utc) {
|
||||||
|
date = makeDate(date);
|
||||||
|
if (utc) {
|
||||||
|
date.setUTCMonth(month);
|
||||||
|
} else {
|
||||||
|
date.setMonth(month);
|
||||||
|
}
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set hours
|
||||||
|
*/
|
||||||
|
export function setHours(date, hours, utc) {
|
||||||
|
date = makeDate(date);
|
||||||
|
if (utc) {
|
||||||
|
date.setUTCHours(hours);
|
||||||
|
} else {
|
||||||
|
date.setHours(hours);
|
||||||
|
}
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set minutes
|
||||||
|
*/
|
||||||
|
export function setMinutes(date, minutes, utc) {
|
||||||
|
date = makeDate(date);
|
||||||
|
if (utc) {
|
||||||
|
date.setUTCMinutes(minutes);
|
||||||
|
} else {
|
||||||
|
date.setMinutes(minutes);
|
||||||
|
}
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set seconds
|
||||||
|
*/
|
||||||
|
export function setSeconds(date, seconds, utc) {
|
||||||
|
date = makeDate(date);
|
||||||
|
if (utc) {
|
||||||
|
date.setUTCSeconds(seconds);
|
||||||
|
} else {
|
||||||
|
date.setSeconds(seconds);
|
||||||
|
}
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
function getFirstThursday(year, utc) {
|
||||||
|
const jan1 = new Date(Date.UTC(year, 0, 1));
|
||||||
|
const dayOfWeek = getDayOfWeek(jan1, utc);
|
||||||
|
const daysToThursday = (11 - dayOfWeek) % 7;
|
||||||
|
return new Date(jan1.getTime() + daysToThursday * 86400000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pad(number, length, padding) {
|
||||||
|
padding = padding || '0';
|
||||||
|
const str = String(number);
|
||||||
|
return padding.repeat(Math.max(0, length - str.length)) + str;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export all functions
|
||||||
|
export default {
|
||||||
|
getDayName,
|
||||||
|
getDayOfWeek,
|
||||||
|
getDayOfYear,
|
||||||
|
getDaysInMonth,
|
||||||
|
getDaysInYear,
|
||||||
|
getFirstDayOfWeek,
|
||||||
|
getFullYear,
|
||||||
|
getHours,
|
||||||
|
getISODate,
|
||||||
|
getISOWeek,
|
||||||
|
getISOYear,
|
||||||
|
getMinutes,
|
||||||
|
getMonth,
|
||||||
|
getMonthName,
|
||||||
|
getSeconds,
|
||||||
|
getMilliseconds,
|
||||||
|
getTimezoneOffset,
|
||||||
|
getTimezoneOffsetString,
|
||||||
|
getUnixTime,
|
||||||
|
getWeek,
|
||||||
|
isLeapYear,
|
||||||
|
isValidDate,
|
||||||
|
makeDate,
|
||||||
|
formatDate,
|
||||||
|
parseDate,
|
||||||
|
getDate,
|
||||||
|
getDay,
|
||||||
|
getISODay,
|
||||||
|
getDayOfTheYear,
|
||||||
|
getFirstDayOfTheYear,
|
||||||
|
setDate,
|
||||||
|
setFullYear,
|
||||||
|
setMonth,
|
||||||
|
setHours,
|
||||||
|
setMinutes,
|
||||||
|
setSeconds
|
||||||
|
};
|
||||||
426
src/ox/core/Encoding.js
Normal file
426
src/ox/core/Encoding.js
Normal file
|
|
@ -0,0 +1,426 @@
|
||||||
|
import { map, char, forEach, range, slice, repeat, loop } from './Array.js';
|
||||||
|
import { BASE_32_DIGITS, BASE_32_ALIASES } from './Constants.js';
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.encodeBase26 <b> Encode a number as bijective base26
|
||||||
|
See <a href="http://en.wikipedia.org/wiki/Bijective_numeration">
|
||||||
|
Bijective numeration</a>.
|
||||||
|
> Ox.encodeBase26(0)
|
||||||
|
''
|
||||||
|
> Ox.encodeBase26(1)
|
||||||
|
'A'
|
||||||
|
> Ox.encodeBase26(26)
|
||||||
|
'Z'
|
||||||
|
> Ox.encodeBase26(27)
|
||||||
|
'AA'
|
||||||
|
> Ox.encodeBase26(4461)
|
||||||
|
'FOO'
|
||||||
|
@*/
|
||||||
|
export function encodeBase26(number) {
|
||||||
|
var string = '';
|
||||||
|
while (number) {
|
||||||
|
string = String.fromCharCode(65 + (number - 1) % 26) + string;
|
||||||
|
number = Math.floor((number - 1) / 26);
|
||||||
|
}
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.decodeBase26 <f> Decodes a bijective base26-encoded number
|
||||||
|
See <a href="http://en.wikipedia.org/wiki/Bijective_numeration">
|
||||||
|
Bijective numeration</a>.
|
||||||
|
> Ox.decodeBase26('foo')
|
||||||
|
4461
|
||||||
|
@*/
|
||||||
|
export function decodeBase26(string) {
|
||||||
|
return string.toUpperCase().split('').reverse().reduce(function(p, c, i) {
|
||||||
|
return p + (c.charCodeAt(0) - 64) * Math.pow(26, i);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.encodeBase32 <b> Encode a number as base32
|
||||||
|
See <a href="http://www.crockford.com/wrmg/base32.html">Base 32</a>.
|
||||||
|
> Ox.encodeBase32(15360)
|
||||||
|
'F00'
|
||||||
|
> Ox.encodeBase32(33819)
|
||||||
|
'110V'
|
||||||
|
@*/
|
||||||
|
export function encodeBase32(number) {
|
||||||
|
return map(number.toString(32), function(char) {
|
||||||
|
return BASE_32_DIGITS[parseInt(char, 32)];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.decodeBase32 <f> Decodes a base32-encoded number
|
||||||
|
See <a href="http://www.crockford.com/wrmg/base32.html">Base 32</a>.
|
||||||
|
> Ox.decodeBase32('foo')
|
||||||
|
15360
|
||||||
|
> Ox.decodeBase32('ILOU')
|
||||||
|
33819
|
||||||
|
> Ox.decodeBase32('?').toString()
|
||||||
|
'NaN'
|
||||||
|
@*/
|
||||||
|
export function decodeBase32(string) {
|
||||||
|
return parseInt(map(string.toUpperCase(), function(char) {
|
||||||
|
var index = BASE_32_DIGITS.indexOf(
|
||||||
|
BASE_32_ALIASES[char] || char
|
||||||
|
);
|
||||||
|
return index == -1 ? ' ' : index.toString(32);
|
||||||
|
}), 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.encodeBase64 <f> Encode a number as base64
|
||||||
|
> Ox.encodeBase64(32394)
|
||||||
|
'foo'
|
||||||
|
@*/
|
||||||
|
export function encodeBase64(number) {
|
||||||
|
return btoa(encodeBase256(number)).replace(/=/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.decodeBase64 <f> Decodes a base64-encoded number
|
||||||
|
> Ox.decodeBase64('foo')
|
||||||
|
32394
|
||||||
|
@*/
|
||||||
|
export function decodeBase64(string) {
|
||||||
|
return decodeBase256(atob(string));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.encodeBase128 <f> Encode a number as base128
|
||||||
|
> Ox.encodeBase128(1685487)
|
||||||
|
'foo'
|
||||||
|
@*/
|
||||||
|
export function encodeBase128(number) {
|
||||||
|
var string = '';
|
||||||
|
while (number) {
|
||||||
|
string = char(number & 127) + string;
|
||||||
|
number >>= 7;
|
||||||
|
}
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.decodeBase128 <f> Decode a base128-encoded number
|
||||||
|
> Ox.decodeBase128('foo')
|
||||||
|
1685487
|
||||||
|
@*/
|
||||||
|
export function decodeBase128(string) {
|
||||||
|
return string.split('').reverse().reduce(function(p, c, i) {
|
||||||
|
return p + (c.charCodeAt(0) << i * 7);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.encodeBase256 <f> Encode a number as base256
|
||||||
|
> Ox.encodeBase256(6713199)
|
||||||
|
'foo'
|
||||||
|
@*/
|
||||||
|
export function encodeBase256(number) {
|
||||||
|
var string = '';
|
||||||
|
while (number) {
|
||||||
|
string = char(number & 255) + string;
|
||||||
|
number >>= 8;
|
||||||
|
}
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.decodeBase256 <f> Decode a base256-encoded number
|
||||||
|
> Ox.decodeBase256('foo')
|
||||||
|
6713199
|
||||||
|
@*/
|
||||||
|
export function decodeBase256(string) {
|
||||||
|
return string.split('').reverse().reduce(function(p, c, i) {
|
||||||
|
return p + (c.charCodeAt(0) << i * 8);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.encodeDeflate <f> Encodes a string, using deflate
|
||||||
|
Since PNGs are deflate-encoded, the `canvas` object's `toDataURL` method
|
||||||
|
provides an efficient implementation. The string is encoded as UTF-8 and
|
||||||
|
written to the RGB channels of a canvas element, then the PNG dataURL is
|
||||||
|
decoded from base64, and some head, tail and chunk names are removed.
|
||||||
|
(str) -> <s> The encoded string
|
||||||
|
str <s> The string to be encoded
|
||||||
|
> Ox.decodeDeflate(Ox.encodeDeflate('foo'), function(str) { Ox.test(str, 'foo'); })
|
||||||
|
undefined
|
||||||
|
@*/
|
||||||
|
export function encodeDeflate(string, callback) {
|
||||||
|
// Make sure we can encode the full unicode range of characters.
|
||||||
|
string = encodeUTF8(string);
|
||||||
|
// We can only safely write to RGB, so we need 1 pixel for 3 bytes.
|
||||||
|
// The string length may not be a multiple of 3, so we need to encode
|
||||||
|
// the number of padding bytes (1 byte), the string, and non-0-bytes
|
||||||
|
// as padding, so that the combined length becomes a multiple of 3.
|
||||||
|
var length = 1 + string.length, c = canvas(Math.ceil(length / 3), 1),
|
||||||
|
data, idat, pad = (3 - length % 3) % 3;
|
||||||
|
string = char(pad) + string + repeat('\u00FF', pad);
|
||||||
|
loop(c.data.length, function(i) {
|
||||||
|
// Write character codes into RGB, and 255 into ALPHA
|
||||||
|
c.data[i] = i % 4 < 3 ? string.charCodeAt(i - parseInt(i / 4)) : 255;
|
||||||
|
});
|
||||||
|
c.context.putImageData(c.imageData, 0, 0);
|
||||||
|
// Get the PNG data from the data URL and decode it from base64.
|
||||||
|
string = atob(c.canvas.toDataURL().split(',')[1]);
|
||||||
|
// Discard bytes 0 to 15 (8 bytes PNG signature, 4 bytes IHDR length, 4
|
||||||
|
// bytes IHDR name), keep bytes 16 to 19 (width), discard bytes 20 to 29
|
||||||
|
// (4 bytes height, 5 bytes flags), keep bytes 29 to 32 (IHDR checksum),
|
||||||
|
// keep the rest (IDAT chunks), discard the last 12 bytes (IEND chunk).
|
||||||
|
data = string.slice(16, 20) + string.slice(29, 33);
|
||||||
|
idat = string.slice(33, -12);
|
||||||
|
while (idat) {
|
||||||
|
// Each IDAT chunk is 4 bytes length, 4 bytes name, length bytes
|
||||||
|
// data and 4 bytes checksum. We can discard the name parts.
|
||||||
|
length = idat.slice(0, 4);
|
||||||
|
data += length + idat.slice(8, 12 + (
|
||||||
|
length = decodeBase256(length)
|
||||||
|
));
|
||||||
|
idat = idat.slice(12 + length);
|
||||||
|
}
|
||||||
|
// Allow for async use, symmetrical to Ox.decodeDeflate
|
||||||
|
callback && callback(data);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.decodeDeflate <f> Decodes an deflate-encoded string
|
||||||
|
Since PNGs are deflate-encoded, the `canvas` object's `drawImage` method
|
||||||
|
provides an efficient implementation. The string will be wrapped as a PNG
|
||||||
|
dataURL, encoded as base64, and drawn onto a canvas element, then the RGB
|
||||||
|
channels will be read, and the result will be decoded from UTF8.
|
||||||
|
(str) -> <u> undefined
|
||||||
|
str <s> The string to be decoded
|
||||||
|
callback <f> Callback function
|
||||||
|
str <s> The decoded string
|
||||||
|
@*/
|
||||||
|
export function decodeDeflate(string, callback) {
|
||||||
|
var image = new Image(),
|
||||||
|
// PNG file signature and IHDR chunk
|
||||||
|
data = '\u0089PNG\r\n\u001A\n\u0000\u0000\u0000\u000DIHDR'
|
||||||
|
+ string.slice(0, 4) + '\u0000\u0000\u0000\u0001'
|
||||||
|
+ '\u0008\u0006\u0000\u0000\u0000' + string.slice(4, 8),
|
||||||
|
// IDAT chunks
|
||||||
|
idat = string.slice(8), length;
|
||||||
|
function error() {
|
||||||
|
throw new RangeError('Deflate codec can\'t decode data.');
|
||||||
|
}
|
||||||
|
while (idat) {
|
||||||
|
// Reinsert the IDAT chunk names
|
||||||
|
length = idat.slice(0, 4);
|
||||||
|
data += length + 'IDAT' + idat.slice(4, 8 + (
|
||||||
|
length = decodeBase256(length)
|
||||||
|
));
|
||||||
|
idat = idat.slice(8 + length);
|
||||||
|
}
|
||||||
|
// IEND chunk
|
||||||
|
data += '\u0000\u0000\u0000\u0000IEND\u00AE\u0042\u0060\u0082';
|
||||||
|
// Unfortunately, we can't synchronously set the source of an image,
|
||||||
|
// draw it onto a canvas, and read its data.
|
||||||
|
image.onload = function() {
|
||||||
|
string = slice(canvas(image).data).map(function(value, index) {
|
||||||
|
// Read one character per RGB byte, ignore ALPHA.
|
||||||
|
return index % 4 < 3 ? char(value) : '';
|
||||||
|
}).join('');
|
||||||
|
try {
|
||||||
|
// Parse the first byte as number of bytes to chop at the end,
|
||||||
|
// and the rest, without these bytes, as an UTF8-encoded string.
|
||||||
|
string = decodeUTF8(
|
||||||
|
string.slice(1, -string.charCodeAt(0) || void 0)
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
error();
|
||||||
|
}
|
||||||
|
callback(string);
|
||||||
|
};
|
||||||
|
image.onerror = error;
|
||||||
|
image.src = 'data:image/png;base64,' + btoa(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
|
||||||
|
function replace(string) {
|
||||||
|
return string.toString().replace(/%(?![0-9A-Fa-f]{2})/g, '%25')
|
||||||
|
.replace(/(%[0-9A-Fa-f]{2})+/g, function(match) {
|
||||||
|
var hex = match.split('%').slice(1), ret;
|
||||||
|
forEach(range(1, hex.length + 1), function(length) {
|
||||||
|
var string = range(length).map(function(i) {
|
||||||
|
return char(parseInt(hex[i], 16));
|
||||||
|
}).join('');
|
||||||
|
try {
|
||||||
|
decodeUTF8(string);
|
||||||
|
ret = match.slice(0, length * 3)
|
||||||
|
+ replace(match.slice(length * 3));
|
||||||
|
return false;
|
||||||
|
} catch(e) {}
|
||||||
|
});
|
||||||
|
return ret || '%25' + hex[0] + replace(match.slice(3));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.decodeURI <f> Decodes URI
|
||||||
|
Unlike window.decodeURI, this doesn't throw on trailing '%'.
|
||||||
|
(string) -> <s> Decoded string
|
||||||
|
@*/
|
||||||
|
decodeURIFn = function(string) {
|
||||||
|
return decodeURI(replace(string));
|
||||||
|
};
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.decodeURIComponent <f> Decodes URI component
|
||||||
|
Unlike window.decodeURIComponent, this doesn't throw on trailing '%'.
|
||||||
|
(string) -> <s> Decoded string
|
||||||
|
@*/
|
||||||
|
decodeURIComponentFn = function(string) {
|
||||||
|
return decodeURIComponent(replace(string));
|
||||||
|
};
|
||||||
|
|
||||||
|
}());
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.decodeURI <f> Decodes URI
|
||||||
|
Unlike window.decodeURI, this doesn't throw on trailing '%'.
|
||||||
|
(string) -> <s> Decoded string
|
||||||
|
@*/
|
||||||
|
export function decodeURICompat(string) {
|
||||||
|
function replace(str) {
|
||||||
|
return str.toString().replace(/%(?![0-9A-Fa-f]{2})/g, '%25')
|
||||||
|
.replace(/(%[0-9A-Fa-f]{2})+/g, function(match) {
|
||||||
|
var hex = match.split('%').slice(1), ret;
|
||||||
|
try {
|
||||||
|
ret = decodeURI('%' + hex.join('%'));
|
||||||
|
} catch (e) {}
|
||||||
|
return ret || '%25' + hex[0] + replace(match.slice(3));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return decodeURI(replace(string));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.decodeURIComponent <f> Decodes URI component
|
||||||
|
Unlike window.decodeURIComponent, this doesn't throw on trailing '%'.
|
||||||
|
(string) -> <s> Decoded string
|
||||||
|
@*/
|
||||||
|
export function decodeURIComponentCompat(string) {
|
||||||
|
function replace(str) {
|
||||||
|
return str.toString().replace(/%(?![0-9A-Fa-f]{2})/g, '%25')
|
||||||
|
.replace(/(%[0-9A-Fa-f]{2})+/g, function(match) {
|
||||||
|
var hex = match.split('%').slice(1), ret;
|
||||||
|
try {
|
||||||
|
ret = decodeURIComponent('%' + hex.join('%'));
|
||||||
|
} catch (e) {}
|
||||||
|
return ret || '%25' + hex[0] + replace(match.slice(3));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return decodeURIComponent(replace(string));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export with proper names
|
||||||
|
export { decodeURICompat as decodeURI, decodeURIComponentCompat as decodeURIComponent };
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.encodeUTF8 <f> Encodes a string as UTF-8
|
||||||
|
see http://en.wikipedia.org/wiki/UTF-8
|
||||||
|
(string) -> <s> UTF-8 encoded string
|
||||||
|
string <s> Any string
|
||||||
|
> Ox.encodeUTF8("YES")
|
||||||
|
"YES"
|
||||||
|
> Ox.encodeUTF8("¥€$")
|
||||||
|
"\u00C2\u00A5\u00E2\u0082\u00AC\u0024"
|
||||||
|
@*/
|
||||||
|
export function encodeUTF8(string) {
|
||||||
|
return map(string, function(char) {
|
||||||
|
var code = char.charCodeAt(0),
|
||||||
|
string = '';
|
||||||
|
if (code < 128) {
|
||||||
|
string = char;
|
||||||
|
} else if (code < 2048) {
|
||||||
|
string = String.fromCharCode(code >> 6 | 192)
|
||||||
|
+ String.fromCharCode(code & 63 | 128);
|
||||||
|
} else {
|
||||||
|
string = String.fromCharCode(code >> 12 | 224)
|
||||||
|
+ String.fromCharCode(code >> 6 & 63 | 128)
|
||||||
|
+ String.fromCharCode(code & 63 | 128);
|
||||||
|
}
|
||||||
|
return string;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.decodeUTF8 <f> Decodes an UTF-8-encoded string
|
||||||
|
see http://en.wikipedia.org/wiki/UTF-8
|
||||||
|
(utf8) -> <s> string
|
||||||
|
utf8 <s> Any UTF-8-encoded string
|
||||||
|
> Ox.decodeUTF8('YES')
|
||||||
|
'YES'
|
||||||
|
> Ox.decodeUTF8('\u00C2\u00A5\u00E2\u0082\u00AC\u0024')
|
||||||
|
'¥€$'
|
||||||
|
@*/
|
||||||
|
export function decodeUTF8(string) {
|
||||||
|
var code, i = 0, length = string.length, ret = '';
|
||||||
|
function error(byte, position) {
|
||||||
|
throw new RangeError(
|
||||||
|
'UTF-8 codec can\'t decode byte 0x' +
|
||||||
|
byte.toString(16).toUpperCase() + ' at position ' + position
|
||||||
|
);
|
||||||
|
}
|
||||||
|
while (i < length) {
|
||||||
|
code = [
|
||||||
|
string.charCodeAt(i),
|
||||||
|
string.charCodeAt(i + 1),
|
||||||
|
string.charCodeAt(i + 2)
|
||||||
|
];
|
||||||
|
if (code[0] < 128) {
|
||||||
|
ret += string[i];
|
||||||
|
i++;
|
||||||
|
} else if (
|
||||||
|
code[0] >= 192 && code[0] < 240
|
||||||
|
&& i < length - (code[0] < 224 ? 1 : 2)
|
||||||
|
) {
|
||||||
|
if (code[1] >= 128 && code[1] < 192) {
|
||||||
|
if (code[0] < 224) {
|
||||||
|
ret += String.fromCharCode(
|
||||||
|
(code[0] & 31) << 6 | code[1] & 63
|
||||||
|
);
|
||||||
|
i += 2;
|
||||||
|
} else if (code[2] >= 128 && code[2] < 192) {
|
||||||
|
ret += String.fromCharCode(
|
||||||
|
(code[0] & 15) << 12 | (code[1] & 63) << 6
|
||||||
|
| code[2] & 63
|
||||||
|
);
|
||||||
|
i += 3;
|
||||||
|
} else {
|
||||||
|
error(code[2], i + 2);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error(code[1], i + 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error(code[0], i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.encodeEmailAddress <f> Returns obfuscated mailto: link
|
||||||
|
> Ox.encodeEmailAddress('mailto:foo@bar.com').indexOf(':') > -1
|
||||||
|
true
|
||||||
|
@*/
|
||||||
|
export function encodeEmailAddress(address, text) {
|
||||||
|
var parts = ['mailto:' + address, text || address].map(function(part) {
|
||||||
|
return map(part, function(char) {
|
||||||
|
var code = char.charCodeAt(0);
|
||||||
|
return char == ':' ? ':'
|
||||||
|
: '&#'
|
||||||
|
+ (Math.random() < 0.5 ? code : 'x' + code.toString(16))
|
||||||
|
+ ';';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return '<a href="' + parts[0] + '">' + parts[1] + '</a>';
|
||||||
|
}
|
||||||
969
src/ox/core/Format.js
Normal file
969
src/ox/core/Format.js
Normal file
|
|
@ -0,0 +1,969 @@
|
||||||
|
/**
|
||||||
|
* Format utilities - ES Module Version
|
||||||
|
* Converted from OxJS Format module while preserving original logic and documentation
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { isUndefined, isNumber } from './Type.js';
|
||||||
|
import { mod, round, sum } from './Math.js';
|
||||||
|
import {
|
||||||
|
getDay, getDayOfTheYear, getDate, getFullYear, getMonth, getHours,
|
||||||
|
getMinutes, getSeconds, getISOYear, getISOWeek, getISODay,
|
||||||
|
getWeek, getTimezoneOffset, getTimezoneOffsetString, makeDate,
|
||||||
|
setDate, setFullYear, setMonth, setHours, setMinutes, setSeconds,
|
||||||
|
getDaysInMonth, getFirstDayOfTheYear
|
||||||
|
} from './Date.js';
|
||||||
|
import { filter, map, forEach } from './Collection.js';
|
||||||
|
import { compact } from './Array.js';
|
||||||
|
import { contains } from './Object.js';
|
||||||
|
import { loop } from './Core.js';
|
||||||
|
import { pad } from './String.js';
|
||||||
|
import { _ } from './Locale.js';
|
||||||
|
|
||||||
|
// Helper functions not yet available in other modules
|
||||||
|
function last(array) {
|
||||||
|
return array[array.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortBy(array, property) {
|
||||||
|
return array.slice().sort(function(a, b) {
|
||||||
|
var aVal = Array.isArray(property) ? property.map(p => a[p]) : a[property];
|
||||||
|
var bVal = Array.isArray(property) ? property.map(p => b[p]) : b[property];
|
||||||
|
return aVal > bVal ? 1 : aVal < bVal ? -1 : 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constants that would normally be in Ox object
|
||||||
|
const WEEKDAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
||||||
|
const SHORT_WEEKDAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||||
|
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
||||||
|
const SHORT_MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||||
|
const AMPM = ['AM', 'PM'];
|
||||||
|
const BCAD = ['BC', 'AD'];
|
||||||
|
const PREFIXES = ['', 'k', 'M', 'G', 'T', 'P'];
|
||||||
|
|
||||||
|
/*@
|
||||||
|
formatArea <f> Formats a number of meters as square meters or kilometers
|
||||||
|
> formatArea(1000)
|
||||||
|
'1,000 m²'
|
||||||
|
> formatArea(1000000)
|
||||||
|
'1 km²'
|
||||||
|
@*/
|
||||||
|
export function formatArea(number, decimals) {
|
||||||
|
var k = number >= 1000000 ? 'k' : '';
|
||||||
|
decimals = isUndefined(decimals) ? 8 : decimals;
|
||||||
|
return formatNumber(
|
||||||
|
(k ? number / 1000000 : number).toPrecision(decimals)
|
||||||
|
) + ' ' + k + 'm\u00B2';
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
formatCount <f> Returns a string like "2 items", "1 item" or "no items".
|
||||||
|
> formatCount(0, 'item')
|
||||||
|
'no items'
|
||||||
|
> formatCount(1, 'item')
|
||||||
|
'1 item'
|
||||||
|
> formatCount(1000, 'city', 'cities')
|
||||||
|
'1,000 cities'
|
||||||
|
@*/
|
||||||
|
export function formatCount(number, singular, plural) {
|
||||||
|
plural = (plural || singular + 's') + (number === 2 ? '{2}' : '');
|
||||||
|
return (number === 0 ? _('no') : formatNumber(number))
|
||||||
|
+ ' ' + _(number === 1 ? singular : plural);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
formatCurrency <f> Formats a number with a currency symbol
|
||||||
|
> formatCurrency(1000, '$', 2)
|
||||||
|
'$1,000.00'
|
||||||
|
@*/
|
||||||
|
export function formatCurrency(number, string, decimals) {
|
||||||
|
return string + formatNumber(number, decimals);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
formatDate <f> Formats a date according to a format string
|
||||||
|
See
|
||||||
|
<a href="http://developer.apple.com/library/mac/#documentation/Darwin/Reference/ManPages/man3/strftime.3.html">strftime</a>
|
||||||
|
and <a href="http://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a>.
|
||||||
|
'E*%' (localized date and time), %Q' (quarter) and '%X'/'%x'
|
||||||
|
(year with 'BC'/'AD') are non-standard.
|
||||||
|
(string) -> <s> formatted date
|
||||||
|
(date, string) -> <s> formatted date
|
||||||
|
(date, string, utc) -> <s> formatted date
|
||||||
|
string <s> format string
|
||||||
|
date <d|n|s> date
|
||||||
|
utc <b> date is utc
|
||||||
|
<script>
|
||||||
|
test.date = new Date('2005/01/02 00:03:04');
|
||||||
|
test.epoch = new Date('1970/01/01 00:00:00');
|
||||||
|
</script>
|
||||||
|
> formatDate(test.date, '%A') // Full weekday
|
||||||
|
'Sunday'
|
||||||
|
> formatDate(test.date, '%a') // Abbreviated weekday
|
||||||
|
'Sun'
|
||||||
|
> formatDate(test.date, '%B') // Full month
|
||||||
|
'January'
|
||||||
|
> formatDate(test.date, '%b') // Abbreviated month
|
||||||
|
'Jan'
|
||||||
|
> formatDate(test.date, '%C') // Century
|
||||||
|
'20'
|
||||||
|
> formatDate(test.date, '%c') // US time and date
|
||||||
|
'01/02/05 12:03:04 AM'
|
||||||
|
> formatDate(test.date, '%D') // US date
|
||||||
|
'01/02/05'
|
||||||
|
> formatDate(test.date, '%d') // Zero-padded day of the month
|
||||||
|
'02'
|
||||||
|
> formatDate(test.date, '%ED') // Localized date and time with seconds
|
||||||
|
'01/02/2005 00:03:04'
|
||||||
|
> formatDate(test.date, '%Ed') // Localized date and time without seconds
|
||||||
|
'01/02/2005 00:03'
|
||||||
|
> formatDate(test.date, '%EL') // Long localized date with weekday
|
||||||
|
'Sunday, January 2, 2005'
|
||||||
|
> formatDate(test.date, '%El') // Long localized date without weekday
|
||||||
|
'January 2, 2005'
|
||||||
|
> formatDate(test.date, '%EM') // Medium localized date with weekday
|
||||||
|
'Sun, Jan 2, 2005'
|
||||||
|
> formatDate(test.date, '%Em') // Medium localized date without weekday
|
||||||
|
'Jan 2, 2005'
|
||||||
|
> formatDate(test.date, '%ES') // Short localized date with century
|
||||||
|
'01/02/2005'
|
||||||
|
> formatDate(test.date, '%Es') // Short localized date without century
|
||||||
|
'01/02/05'
|
||||||
|
> formatDate(test.date, '%ET') // Localized time with seconds
|
||||||
|
'12:03:04 AM'
|
||||||
|
> formatDate(test.date, '%Et') // Localized time without seconds
|
||||||
|
'12:03 AM'
|
||||||
|
> formatDate(test.date, '%e') // Space-padded day of the month
|
||||||
|
' 2'
|
||||||
|
> formatDate(test.date, '%F') // Date
|
||||||
|
'2005-01-02'
|
||||||
|
> formatDate(test.date, '%G') // Full ISO-8601 year
|
||||||
|
'2004'
|
||||||
|
> formatDate(test.date, '%g') // Abbreviated ISO-8601 year
|
||||||
|
'04'
|
||||||
|
> formatDate(test.date, '%H') // Zero-padded hour (24-hour clock)
|
||||||
|
'00'
|
||||||
|
> formatDate(test.date, '%h') // Abbreviated month
|
||||||
|
'Jan'
|
||||||
|
> formatDate(test.date, '%I') // Zero-padded hour (12-hour clock)
|
||||||
|
'12'
|
||||||
|
> formatDate(test.date, '%j') // Zero-padded day of the year
|
||||||
|
'002'
|
||||||
|
> formatDate(test.date, '%k') // Space-padded hour (24-hour clock)
|
||||||
|
' 0'
|
||||||
|
> formatDate(test.date, '%l') // Space-padded hour (12-hour clock)
|
||||||
|
'12'
|
||||||
|
> formatDate(test.date, '%M') // Zero-padded minute
|
||||||
|
'03'
|
||||||
|
> formatDate(test.date, '%m') // Zero-padded month
|
||||||
|
'01'
|
||||||
|
> formatDate(test.date, '%n') // Newline
|
||||||
|
'\n'
|
||||||
|
> formatDate(test.date, '%p') // AM or PM
|
||||||
|
'AM'
|
||||||
|
> formatDate(test.date, '%Q') // Quarter of the year
|
||||||
|
'1'
|
||||||
|
> formatDate(test.date, '%R') // Zero-padded hour and minute
|
||||||
|
'00:03'
|
||||||
|
> formatDate(test.date, '%r') // US time
|
||||||
|
'12:03:04 AM'
|
||||||
|
> formatDate(test.date, '%S') // Zero-padded second
|
||||||
|
'04'
|
||||||
|
> formatDate(test.epoch, '%s', true) // Number of seconds since the Epoch
|
||||||
|
'0'
|
||||||
|
> formatDate(test.date, '%T') // Time
|
||||||
|
'00:03:04'
|
||||||
|
> formatDate(test.date, '%t') // Tab
|
||||||
|
'\t'
|
||||||
|
> formatDate(test.date, '%U') // Zero-padded week of the year (00-53, Sunday as first day)
|
||||||
|
'01'
|
||||||
|
> formatDate(test.date, '%u') // Decimal weekday (1-7, Monday as first day)
|
||||||
|
'7'
|
||||||
|
> formatDate(test.date, '%V') // Zero-padded ISO-8601 week of the year
|
||||||
|
'53'
|
||||||
|
> formatDate(test.date, '%v') // Formatted date
|
||||||
|
' 2-Jan-2005'
|
||||||
|
> formatDate(test.date, '%W') // Zero-padded week of the year (00-53, Monday as first day)
|
||||||
|
'00'
|
||||||
|
> formatDate(test.date, '%w') // Decimal weekday (0-6, Sunday as first day)
|
||||||
|
'0'
|
||||||
|
> formatDate(test.date, '%X') // Full year with BC or AD
|
||||||
|
'2005 AD'
|
||||||
|
> formatDate(test.date, '%x') // Full year with BC or AD if year < 1000
|
||||||
|
'2005'
|
||||||
|
> formatDate(test.date, '%Y') // Full year
|
||||||
|
'2005'
|
||||||
|
> formatDate(test.date, '%y') // Abbreviated year
|
||||||
|
'05'
|
||||||
|
> formatDate(test.date, '%Z', true) // Time zone name
|
||||||
|
'UTC'
|
||||||
|
> formatDate(test.date, '%z', true) // Time zone offset
|
||||||
|
'+0000'
|
||||||
|
> formatDate(test.date, '%+').replace(/ [A-Z]+ /, ' XYZ ') // Formatted date and time
|
||||||
|
'Sun Jan 2 00:03:04 XYZ 2005'
|
||||||
|
> formatDate(test.date, '%%')
|
||||||
|
'%'
|
||||||
|
@*/
|
||||||
|
|
||||||
|
// Format patterns for formatDate function
|
||||||
|
var formatPatterns = [
|
||||||
|
['%', function() {
|
||||||
|
return '%{%}';
|
||||||
|
}],
|
||||||
|
['c', function() {
|
||||||
|
return '%D %r';
|
||||||
|
}],
|
||||||
|
['D', function() {
|
||||||
|
return '%m/%d/%y';
|
||||||
|
}],
|
||||||
|
['ED', function() {
|
||||||
|
return '%ES %T';
|
||||||
|
}],
|
||||||
|
['Ed', function() {
|
||||||
|
return '%ES %R';
|
||||||
|
}],
|
||||||
|
['EL', function() {
|
||||||
|
return _('%A, %B %e, %Y');
|
||||||
|
}],
|
||||||
|
['El', function() {
|
||||||
|
return _('%B %e, %Y');
|
||||||
|
}],
|
||||||
|
['EM', function() {
|
||||||
|
return _('%a, %b %e, %Y');
|
||||||
|
}],
|
||||||
|
['Em', function() {
|
||||||
|
return _('%b %e, %Y');
|
||||||
|
}],
|
||||||
|
['ES', function() {
|
||||||
|
return _('%m/%d/%Y');
|
||||||
|
}],
|
||||||
|
['Es', function() {
|
||||||
|
return _('%m/%d/%y');
|
||||||
|
}],
|
||||||
|
['ET', function() {
|
||||||
|
return _('%I:%M:%S %p');
|
||||||
|
}],
|
||||||
|
['Et', function() {
|
||||||
|
return _('%I:%M %p');
|
||||||
|
}],
|
||||||
|
['F', function() {
|
||||||
|
return '%Y-%m-%d';
|
||||||
|
}],
|
||||||
|
['h', function() {
|
||||||
|
return '%b';
|
||||||
|
}],
|
||||||
|
['R', function() {
|
||||||
|
return '%H:%M';
|
||||||
|
}],
|
||||||
|
['r', function() {
|
||||||
|
return '%I:%M:%S %p';
|
||||||
|
}],
|
||||||
|
['T', function() {
|
||||||
|
return '%H:%M:%S';
|
||||||
|
}],
|
||||||
|
['v', function() {
|
||||||
|
return '%e-%b-%Y';
|
||||||
|
}],
|
||||||
|
['\\+', function() {
|
||||||
|
return '%a %b %e %H:%M:%S %Z %Y';
|
||||||
|
}],
|
||||||
|
['A', function(date, utc) {
|
||||||
|
return _(WEEKDAYS[(getDay(date, utc) + 6) % 7]);
|
||||||
|
}],
|
||||||
|
['a', function(date, utc) {
|
||||||
|
return _(SHORT_WEEKDAYS[(getDay(date, utc) + 6) % 7]);
|
||||||
|
}],
|
||||||
|
['B', function(date, utc) {
|
||||||
|
return _(MONTHS[getMonth(date, utc)]);
|
||||||
|
}],
|
||||||
|
['b', function(date, utc) {
|
||||||
|
return _(SHORT_MONTHS[getMonth(date, utc)]);
|
||||||
|
}],
|
||||||
|
['C', function(date, utc) {
|
||||||
|
return Math.floor(getFullYear(date, utc) / 100).toString();
|
||||||
|
}],
|
||||||
|
['d', function(date, utc) {
|
||||||
|
return pad(getDate(date, utc), 2);
|
||||||
|
}],
|
||||||
|
['e', function(date, utc) {
|
||||||
|
return pad(getDate(date, utc), 2, ' ');
|
||||||
|
}],
|
||||||
|
['G', function(date, utc) {
|
||||||
|
return getISOYear(date, utc);
|
||||||
|
}],
|
||||||
|
['g', function(date, utc) {
|
||||||
|
return getISOYear(date, utc).toString().slice(-2);
|
||||||
|
}],
|
||||||
|
['H', function(date, utc) {
|
||||||
|
return pad(getHours(date, utc), 2);
|
||||||
|
}],
|
||||||
|
['I', function(date, utc) {
|
||||||
|
return pad((getHours(date, utc) + 11) % 12 + 1, 2);
|
||||||
|
}],
|
||||||
|
['j', function(date, utc) {
|
||||||
|
return pad(getDayOfTheYear(date, utc), 3);
|
||||||
|
}],
|
||||||
|
['k', function(date, utc) {
|
||||||
|
return pad(getHours(date, utc), 2, ' ');
|
||||||
|
}],
|
||||||
|
['l', function(date, utc) {
|
||||||
|
return pad(((getHours(date, utc) + 11) % 12 + 1), 2, ' ');
|
||||||
|
}],
|
||||||
|
['M', function(date, utc) {
|
||||||
|
return pad(getMinutes(date, utc), 2);
|
||||||
|
}],
|
||||||
|
['m', function(date, utc) {
|
||||||
|
return pad((getMonth(date, utc) + 1), 2);
|
||||||
|
}],
|
||||||
|
['p', function(date, utc) {
|
||||||
|
return _(AMPM[Math.floor(getHours(date, utc) / 12)]);
|
||||||
|
}],
|
||||||
|
['Q', function(date, utc) {
|
||||||
|
return Math.floor(getMonth(date, utc) / 4) + 1;
|
||||||
|
}],
|
||||||
|
['S', function(date, utc) {
|
||||||
|
return pad(getSeconds(date, utc), 2);
|
||||||
|
}],
|
||||||
|
['s', function(date, utc) {
|
||||||
|
return Math.floor((+date - (
|
||||||
|
utc ? getTimezoneOffset(date) : 0
|
||||||
|
)) / 1000);
|
||||||
|
}],
|
||||||
|
['U', function(date, utc) {
|
||||||
|
return pad(getWeek(date, utc), 2);
|
||||||
|
}],
|
||||||
|
['u', function(date, utc) {
|
||||||
|
return getISODay(date, utc);
|
||||||
|
}],
|
||||||
|
['V', function(date, utc) {
|
||||||
|
return pad(getISOWeek(date, utc), 2);
|
||||||
|
}],
|
||||||
|
['W', function(date, utc) {
|
||||||
|
return pad(Math.floor((getDayOfTheYear(date, utc)
|
||||||
|
+ (getFirstDayOfTheYear(date, utc) || 7) - 2) / 7), 2);
|
||||||
|
}],
|
||||||
|
['w', function(date, utc) {
|
||||||
|
return getDay(date, utc);
|
||||||
|
}],
|
||||||
|
['X', function(date, utc) {
|
||||||
|
var y = getFullYear(date, utc);
|
||||||
|
return Math.abs(y) + ' ' + _(BCAD[y < 0 ? 0 : 1]);
|
||||||
|
}],
|
||||||
|
['x', function(date, utc) {
|
||||||
|
var y = getFullYear(date, utc);
|
||||||
|
return Math.abs(y) + (
|
||||||
|
y < 1000 ? ' ' + _(BCAD[y < 0 ? 0 : 1]) : ''
|
||||||
|
);
|
||||||
|
}],
|
||||||
|
['Y', function(date, utc) {
|
||||||
|
return getFullYear(date, utc);
|
||||||
|
}],
|
||||||
|
['y', function(date, utc) {
|
||||||
|
return getFullYear(date, utc).toString().slice(-2);
|
||||||
|
}],
|
||||||
|
['Z', function(date, utc) {
|
||||||
|
return utc ? 'UTC'
|
||||||
|
: (date.toString().split('(')[1] || '').replace(')', '');
|
||||||
|
}],
|
||||||
|
['z', function(date, utc) {
|
||||||
|
return utc ? '+0000' : getTimezoneOffsetString(date);
|
||||||
|
}],
|
||||||
|
['n', function() {
|
||||||
|
return '\n';
|
||||||
|
}],
|
||||||
|
['t', function() {
|
||||||
|
return '\t';
|
||||||
|
}],
|
||||||
|
['\\{%\\}', function() {
|
||||||
|
return '%';
|
||||||
|
}]
|
||||||
|
].map(function(value) {
|
||||||
|
return [new RegExp('%' + value[0], 'g'), value[1]];
|
||||||
|
});
|
||||||
|
|
||||||
|
export function formatDate(date, string, utc) {
|
||||||
|
if (date === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
date = makeDate(date);
|
||||||
|
formatPatterns.forEach(function(value) {
|
||||||
|
string = string.replace(value[0], function() {
|
||||||
|
return value[1](date, utc);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
formatDateRange <f> Formats a date range as a string
|
||||||
|
A date range is a pair of arbitrary-presicion date strings
|
||||||
|
> formatDateRange('2000', '2001')
|
||||||
|
'2000'
|
||||||
|
> formatDateRange('2000', '2002')
|
||||||
|
'2000 - 2002'
|
||||||
|
> formatDateRange('2000-01', '2000-02')
|
||||||
|
'January 2000'
|
||||||
|
> formatDateRange('2000-01', '2000-03')
|
||||||
|
'January - March 2000'
|
||||||
|
> formatDateRange('2000-01-01', '2000-01-02')
|
||||||
|
'Sat, Jan 1, 2000'
|
||||||
|
> formatDateRange('2000-01-01', '2000-01-03')
|
||||||
|
'Sat, Jan 1 - Mon, Jan 3, 2000'
|
||||||
|
> formatDateRange('2000-01-01 00', '2000-01-01 01')
|
||||||
|
'Sat, Jan 1, 2000, 00:00'
|
||||||
|
> formatDateRange('2000-01-01 00', '2000-01-01 02')
|
||||||
|
'Sat, Jan 1, 2000, 00:00 - 02:00'
|
||||||
|
> formatDateRange('2000-01-01 00:00', '2000-01-01 00:01')
|
||||||
|
'Sat, Jan 1, 2000, 00:00'
|
||||||
|
> formatDateRange('2000-01-01 00:00', '2000-01-01 00:02')
|
||||||
|
'Sat, Jan 1, 2000, 00:00 - 00:02'
|
||||||
|
> formatDateRange('2000-01-01 00:00:00', '2000-01-01 00:00:01')
|
||||||
|
'Sat, Jan 1, 2000, 00:00:00'
|
||||||
|
> formatDateRange('2000-01-01 00:00:00', '2000-01-01 00:00:02')
|
||||||
|
'Sat, Jan 1, 2000, 00:00:00 - 00:00:02'
|
||||||
|
> formatDateRange('1999-12', '2000-01')
|
||||||
|
'December 1999'
|
||||||
|
> formatDateRange('1999-12-31', '2000-01-01')
|
||||||
|
'Fri, Dec 31, 1999'
|
||||||
|
> formatDateRange('1999-12-31 23:59', '2000-01-01 00:00')
|
||||||
|
'Fri, Dec 31, 1999, 23:59'
|
||||||
|
> formatDateRange('-50', '50')
|
||||||
|
'50 BC - 50 AD'
|
||||||
|
> formatDateRange('-50-01-01', '-50-12-31')
|
||||||
|
'Sun, Jan 1 - Sun, Dec 31, 50 BC'
|
||||||
|
> formatDateRange('-50-01-01 00:00:00', '-50-01-01 23:59:59')
|
||||||
|
'Sun, Jan 1, 50 BC, 00:00:00 - 23:59:59'
|
||||||
|
@*/
|
||||||
|
export function formatDateRange(start, end, utc) {
|
||||||
|
end = end || formatDate(new Date(), '%Y-%m-%d');
|
||||||
|
var isOneUnit = false,
|
||||||
|
range = [start, end],
|
||||||
|
strings,
|
||||||
|
dates = range.map(function(str){
|
||||||
|
return parseDate(str, utc);
|
||||||
|
}),
|
||||||
|
parts = range.map(function(str) {
|
||||||
|
var parts = compact(
|
||||||
|
/(-?\d+)-?(\d+)?-?(\d+)? ?(\d+)?:?(\d+)?:?(\d+)?/.exec(str)
|
||||||
|
);
|
||||||
|
parts.shift();
|
||||||
|
return parts.map(function(part) {
|
||||||
|
return parseInt(part, 10);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
precision = parts.map(function(parts) {
|
||||||
|
return parts.length;
|
||||||
|
}),
|
||||||
|
y = parts[0][0] < 0 ? '%X' : '%Y',
|
||||||
|
formats = [
|
||||||
|
y,
|
||||||
|
'%B ' + y,
|
||||||
|
'%a, %b %e, ' + y,
|
||||||
|
'%a, %b %e, ' + y + ', %H:%M',
|
||||||
|
'%a, %b %e, ' + y + ', %H:%M',
|
||||||
|
'%a, %b %e, ' + y + ', %H:%M:%S',
|
||||||
|
];
|
||||||
|
if (precision[0] == precision[1]) {
|
||||||
|
isOneUnit = true;
|
||||||
|
loop(precision[0], function(i) {
|
||||||
|
if (
|
||||||
|
(i < precision[0] - 1 && parts[0][i] != parts[1][i])
|
||||||
|
|| (i == precision[0] - 1 && parts[0][i] != parts[1][i] - 1)
|
||||||
|
) {
|
||||||
|
isOneUnit = false;
|
||||||
|
return false; // break
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (isOneUnit) {
|
||||||
|
strings = [formatDate(dates[0], formats[precision[0] - 1], utc)];
|
||||||
|
} else {
|
||||||
|
strings = [
|
||||||
|
formatDate(dates[0], formats[precision[0] - 1], utc),
|
||||||
|
formatDate(dates[1], formats[precision[1] - 1], utc)
|
||||||
|
];
|
||||||
|
// if same year, and neither date is more precise than day,
|
||||||
|
// then omit first year
|
||||||
|
if (
|
||||||
|
parts[0][0] == parts[1][0]
|
||||||
|
&& precision[0] <= 3
|
||||||
|
&& precision[1] <= 3
|
||||||
|
) {
|
||||||
|
strings[0] = formatDate(
|
||||||
|
dates[0], formats[precision[0] - 1].replace(
|
||||||
|
new RegExp(',? ' + y), ''
|
||||||
|
), utc
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// if same day then omit second day
|
||||||
|
if (
|
||||||
|
parts[0][0] == parts[1][0]
|
||||||
|
&& parts[0][1] == parts[1][1]
|
||||||
|
&& parts[0][2] == parts[1][2]
|
||||||
|
) {
|
||||||
|
strings[1] = strings[1].split(', ').pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// %e is a space-padded day
|
||||||
|
return strings.join(' - ').replace(/ /g, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
formatDateRangeDuration <f> Formats the duration of a date range as a string
|
||||||
|
A date range is a pair of arbitrary-presicion date strings
|
||||||
|
> formatDateRangeDuration('2000-01-01 00:00:00', '2001-03-04 04:05:06')
|
||||||
|
'1 year 2 months 3 days 4 hours 5 minutes 6 seconds'
|
||||||
|
> formatDateRangeDuration('2000', '2001-01-01 00:00:01')
|
||||||
|
'1 year 1 second'
|
||||||
|
> formatDateRangeDuration('1999', '2000', true)
|
||||||
|
'1 year'
|
||||||
|
> formatDateRangeDuration('2000', '2001', true)
|
||||||
|
'1 year'
|
||||||
|
> formatDateRangeDuration('1999-02', '1999-03', true)
|
||||||
|
'1 month'
|
||||||
|
> formatDateRangeDuration('2000-02', '2000-03', true)
|
||||||
|
'1 month'
|
||||||
|
@*/
|
||||||
|
export function formatDateRangeDuration(start, end, utc) {
|
||||||
|
end = end || formatDate(new Date(), '%Y-%m-%d');
|
||||||
|
var date = parseDate(start, utc),
|
||||||
|
dates = [start, end].map(function(string) {
|
||||||
|
return parseDate(string, utc);
|
||||||
|
}),
|
||||||
|
keys = ['year', 'month', 'day', 'hour', 'minute', 'second'],
|
||||||
|
parts = ['FullYear', 'Month', 'Date', 'Hours', 'Minutes', 'Seconds'],
|
||||||
|
values = [];
|
||||||
|
date && keys.forEach(function(key, i) {
|
||||||
|
while (true) {
|
||||||
|
if (key == 'month') {
|
||||||
|
// set the day to the same day in the next month,
|
||||||
|
// or to its last day if the next month is shorter
|
||||||
|
var day = getDate(date, utc);
|
||||||
|
setDate(date, Math.min(
|
||||||
|
day,
|
||||||
|
getDaysInMonth(
|
||||||
|
getFullYear(date, utc),
|
||||||
|
getMonth(date, utc) + 2,
|
||||||
|
utc
|
||||||
|
)
|
||||||
|
), utc);
|
||||||
|
}
|
||||||
|
// advance the date by one unit
|
||||||
|
if (key == 'year') {
|
||||||
|
setFullYear(date, getFullYear(date, utc) + 1, utc);
|
||||||
|
} else if (key == 'month') {
|
||||||
|
setMonth(date, getMonth(date, utc) + 1, utc);
|
||||||
|
} else if (key == 'day') {
|
||||||
|
setDate(date, getDate(date, utc) + 1, utc);
|
||||||
|
} else if (key == 'hour') {
|
||||||
|
setHours(date, getHours(date, utc) + 1, utc);
|
||||||
|
} else if (key == 'minute') {
|
||||||
|
setMinutes(date, getMinutes(date, utc) + 1, utc);
|
||||||
|
} else if (key == 'second') {
|
||||||
|
setSeconds(date, getSeconds(date, utc) + 1, utc);
|
||||||
|
}
|
||||||
|
if (date <= dates[1]) {
|
||||||
|
// still within the range, add one unit
|
||||||
|
values[i] = (values[i] || 0) + 1;
|
||||||
|
} else {
|
||||||
|
// outside the range, rewind the date by one unit
|
||||||
|
if (key == 'year') {
|
||||||
|
setFullYear(date, getFullYear(date, utc) - 1, utc);
|
||||||
|
} else if (key == 'month') {
|
||||||
|
setMonth(date, getMonth(date, utc) - 1, utc);
|
||||||
|
} else if (key == 'day') {
|
||||||
|
setDate(date, getDate(date, utc) - 1, utc);
|
||||||
|
} else if (key == 'hour') {
|
||||||
|
setHours(date, getHours(date, utc) - 1, utc);
|
||||||
|
} else if (key == 'minute') {
|
||||||
|
setMinutes(date, getMinutes(date, utc) - 1, utc);
|
||||||
|
} else if (key == 'second') {
|
||||||
|
setSeconds(date, getSeconds(date, utc) - 1, utc);
|
||||||
|
}
|
||||||
|
// and revert to original day
|
||||||
|
key == 'month' && setDate(date, day, utc);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return filter(map(values, function(value, i) {
|
||||||
|
return value ? value + ' ' + keys[i] + (value > 1 ? 's' : '') : '';
|
||||||
|
})).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
formatDegrees <f> Formats degrees as D°MM'SS"
|
||||||
|
> formatDegrees(-111.11, 'lng')
|
||||||
|
"111°06'36\"W"
|
||||||
|
@*/
|
||||||
|
export function formatDegrees(degrees, mode) {
|
||||||
|
var days = 0,
|
||||||
|
seconds = Math.round(Math.abs(degrees) * 3600),
|
||||||
|
sign = degrees < 0 ? '-' : '',
|
||||||
|
array = formatDuration(seconds).split(':');
|
||||||
|
if (array.length == 4) {
|
||||||
|
days = parseInt(array.shift(), 10);
|
||||||
|
}
|
||||||
|
array[0] = days * 24 + parseInt(array[0], 10);
|
||||||
|
return (!mode ? sign : '')
|
||||||
|
+ array[0] + '°' + array[1] + "'" + array[2] + '"'
|
||||||
|
+ (
|
||||||
|
mode == 'lat' ? (degrees < 0 ? 'S' : 'N')
|
||||||
|
: mode == 'lng' ? (degrees < 0 ? 'W' : 'E')
|
||||||
|
: ''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
formatDimensions <f> Formats valus as dimension
|
||||||
|
> formatDimensions([1920, 1080], 'px')
|
||||||
|
"1,920 × 1,080 px"
|
||||||
|
@*/
|
||||||
|
export function formatDimensions(array, string) {
|
||||||
|
return array.map(function(value) {
|
||||||
|
return formatNumber(value);
|
||||||
|
}).join(' × ') + (string ? ' ' + string : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatResolution = formatDimensions;
|
||||||
|
|
||||||
|
/*@
|
||||||
|
formatDuration <f> Formats a duration as a string
|
||||||
|
> formatDuration(3599.999)
|
||||||
|
'01:00:00'
|
||||||
|
> formatDuration(3599.999, 2)
|
||||||
|
'01:00:00.00'
|
||||||
|
> formatDuration(3599.999, 3)
|
||||||
|
'00:59:59.999'
|
||||||
|
> formatDuration(3599.999, 'short')
|
||||||
|
'1h'
|
||||||
|
> formatDuration(3599.999, 3, 'short')
|
||||||
|
'59m 59.999s'
|
||||||
|
> formatDuration(3599.999, 'long')
|
||||||
|
'1 hour'
|
||||||
|
> formatDuration(3599.999, 3, 'long')
|
||||||
|
'59 minutes 59.999 seconds'
|
||||||
|
> formatDuration(1640673)
|
||||||
|
'18:23:44:33'
|
||||||
|
> formatDuration(86520, 2)
|
||||||
|
'1:00:02:00.00'
|
||||||
|
> formatDuration(86520, 'long')
|
||||||
|
'1 day 2 minutes'
|
||||||
|
> formatDuration(31543203, 2)
|
||||||
|
'1:000:02:00:03.00'
|
||||||
|
> formatDuration(31543203, 'long')
|
||||||
|
'1 year 2 hours 3 seconds'
|
||||||
|
> formatDuration(0, 2)
|
||||||
|
'00:00:00.00'
|
||||||
|
> formatDuration(0, 'long')
|
||||||
|
''
|
||||||
|
@*/
|
||||||
|
export function formatDuration(seconds/*, decimals, format*/) {
|
||||||
|
var lastArg = last(arguments),
|
||||||
|
format = lastArg == 'short' || lastArg == 'long' ? lastArg : 'none',
|
||||||
|
decimals = isNumber(arguments[1]) ? arguments[1] : 0,
|
||||||
|
seconds = round(Math.abs(seconds), decimals),
|
||||||
|
values = [
|
||||||
|
Math.floor(seconds / 31536000),
|
||||||
|
Math.floor(seconds % 31536000 / 86400),
|
||||||
|
Math.floor(seconds % 86400 / 3600),
|
||||||
|
Math.floor(seconds % 3600 / 60),
|
||||||
|
formatNumber(seconds % 60, decimals)
|
||||||
|
],
|
||||||
|
string = format == 'short' ? ['y', 'd', 'h', 'm', 's']
|
||||||
|
: format == 'long' ? ['year', 'day', 'hour', 'minute', 'second']
|
||||||
|
: [],
|
||||||
|
padArray = [
|
||||||
|
values[0].toString().length,
|
||||||
|
values[0] ? 3 : values[1].toString().length,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
decimals ? decimals + 3 : 2
|
||||||
|
];
|
||||||
|
while (!values[0] && values.length > (format == 'none' ? 3 : 1)) {
|
||||||
|
values.shift();
|
||||||
|
string.shift();
|
||||||
|
padArray.shift();
|
||||||
|
}
|
||||||
|
return filter(map(values, function(value, index) {
|
||||||
|
var ret;
|
||||||
|
if (format == 'none') {
|
||||||
|
ret = pad(value, 'left', padArray[index], '0');
|
||||||
|
} else if (isNumber(value) ? value : parseFloat(value)) {
|
||||||
|
ret = value + (format == 'long' ? ' ' : '') + _(string[index] + (
|
||||||
|
format == 'long'
|
||||||
|
? (value == 1 ? '' : value == 2 ? 's{2}' : 's')
|
||||||
|
: ''
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
ret = '';
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
})).join(format == 'none' ? ':' : ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
formatISBN <f> Formats a string as an ISBN of a given length (10 or 13)
|
||||||
|
(isbn, length) -> <s> ISBN
|
||||||
|
isbn <s> ISBN
|
||||||
|
length <n> length (10 or 13)
|
||||||
|
> formatISBN('0-306-40615-2', 13, true)
|
||||||
|
'978-0-306-40615-7'
|
||||||
|
> formatISBN('978-0-306-40615-7', 10)
|
||||||
|
'0306406152'
|
||||||
|
@*/
|
||||||
|
export function formatISBN(isbn, length, dashes) {
|
||||||
|
var ret = '';
|
||||||
|
function getCheckDigit(isbn) {
|
||||||
|
var modValue = isbn.length == 10 ? 11 : 10
|
||||||
|
return (mod(modValue - sum(
|
||||||
|
isbn.slice(0, -1).split('').map(function(digit, index) {
|
||||||
|
return isbn.length == 10
|
||||||
|
? parseInt(digit) * (10 - index)
|
||||||
|
: parseInt(digit) * (index % 2 == 0 ? 1 : 3);
|
||||||
|
})
|
||||||
|
), modValue) + '').replace('10', 'X');
|
||||||
|
}
|
||||||
|
isbn = isbn.toUpperCase().replace(/[^\dX]/g, '');
|
||||||
|
if (isbn.length == 10) {
|
||||||
|
isbn = isbn.slice(0, -1).replace(/\D/g, '') + isbn.slice(-1);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(isbn.length == 10 || isbn.length == 13)
|
||||||
|
&& isbn.slice(-1) == getCheckDigit(isbn)
|
||||||
|
) {
|
||||||
|
if (isbn.length == length) {
|
||||||
|
ret = isbn
|
||||||
|
} else if (isbn.length == 10 || isbn.slice(0, 3) == '978') {
|
||||||
|
isbn = isbn.length == 10 ? '978' + isbn : isbn.slice(3);
|
||||||
|
ret = isbn.slice(0, -1) + getCheckDigit(isbn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dashes ? [
|
||||||
|
ret.slice(-13, -10),
|
||||||
|
ret.slice(-10, -9),
|
||||||
|
ret.slice(-9, -6),
|
||||||
|
ret.slice(-6, -1),
|
||||||
|
ret.slice(-1)
|
||||||
|
].join('-').replace(/^-+/, '') : ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
formatNumber <f> Formats a number with thousands separators
|
||||||
|
(num, dec) -> <s> format number to string
|
||||||
|
num <n> number
|
||||||
|
dec <n|0> number of decimals
|
||||||
|
> formatNumber(123456789, 3)
|
||||||
|
"123,456,789.000"
|
||||||
|
> formatNumber(-2000000 / 3, 3)
|
||||||
|
"-666,666.667"
|
||||||
|
> formatNumber(666666.666)
|
||||||
|
"666,667"
|
||||||
|
@*/
|
||||||
|
export function formatNumber(number, decimals) {
|
||||||
|
var array = [],
|
||||||
|
abs = Math.abs(number),
|
||||||
|
split = abs.toFixed(decimals).split('.');
|
||||||
|
while (split[0]) {
|
||||||
|
array.unshift(split[0].slice(-3));
|
||||||
|
split[0] = split[0].slice(0, -3);
|
||||||
|
}
|
||||||
|
split[0] = array.join(_(','));
|
||||||
|
return (number < 0 ? '-' : '') + split.join(_('.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
formatOrdinal <f> Formats a number as an ordinal
|
||||||
|
> formatOrdinal(1)
|
||||||
|
"1st"
|
||||||
|
> formatOrdinal(2)
|
||||||
|
"2nd"
|
||||||
|
> formatOrdinal(3)
|
||||||
|
"3rd"
|
||||||
|
> formatOrdinal(4)
|
||||||
|
"4th"
|
||||||
|
> formatOrdinal(11)
|
||||||
|
"11th"
|
||||||
|
> formatOrdinal(12)
|
||||||
|
"12th"
|
||||||
|
> formatOrdinal(13)
|
||||||
|
"13th"
|
||||||
|
@*/
|
||||||
|
export function formatOrdinal(number) {
|
||||||
|
var string = formatNumber(number),
|
||||||
|
length = string.length,
|
||||||
|
lastChar = string[length - 1],
|
||||||
|
ten = length > 1 && string[length - 2] == '1',
|
||||||
|
twenty = length > 1 && !ten;
|
||||||
|
if (lastChar == '1' && !ten) {
|
||||||
|
string += _('st' + (twenty ? '{21}' : ''));
|
||||||
|
} else if (lastChar == '2' && !ten) {
|
||||||
|
string += _('nd' + (twenty ? '{22}' : ''));
|
||||||
|
} else if (lastChar == '3' && !ten) {
|
||||||
|
string += _('rd' + (twenty ? '{23}' : ''));
|
||||||
|
} else {
|
||||||
|
string += _(
|
||||||
|
'th' + (contains('123', lastChar) && ten ? '{1' + lastChar + '}' : '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
formatPercent <f> Formats the relation of two numbers as a percentage
|
||||||
|
> formatPercent(1, 1000, 2)
|
||||||
|
"0.10%"
|
||||||
|
@*/
|
||||||
|
export function formatPercent(number, total, decimals) {
|
||||||
|
return formatNumber(number / total * 100, decimals) + _('%');
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
formatRoman <f> Formats a number as a roman numeral
|
||||||
|
> formatRoman(1888)
|
||||||
|
'MDCCCLXXXVIII'
|
||||||
|
> formatRoman(1999)
|
||||||
|
'MCMXCIX'
|
||||||
|
> formatRoman(2000)
|
||||||
|
'MM'
|
||||||
|
> formatRoman(-1)
|
||||||
|
''
|
||||||
|
> formatRoman(0)
|
||||||
|
''
|
||||||
|
> formatRoman(9.9)
|
||||||
|
'IX'
|
||||||
|
> formatRoman(10000)
|
||||||
|
'MMMMMMMMMM'
|
||||||
|
@*/
|
||||||
|
export function formatRoman(number) {
|
||||||
|
var string = '';
|
||||||
|
forEach({
|
||||||
|
M: 1000, CM: 900, D: 500, CD: 400, C: 100, XC: 90,
|
||||||
|
L: 50, XL: 40, X: 10, IX: 9, V: 5, IV: 4, I: 1
|
||||||
|
}, function(value, roman) {
|
||||||
|
while (number >= value) {
|
||||||
|
string += roman;
|
||||||
|
number -= value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
formatSRT <f> Formats subtitles as SRT
|
||||||
|
@*/
|
||||||
|
export function formatSRT(subtitles) {
|
||||||
|
return '\ufeff' + sortBy(subtitles, ['in', 'out']).map(function(subtitle, index) {
|
||||||
|
return [
|
||||||
|
index + 1,
|
||||||
|
['in', 'out'].map(function(key) {
|
||||||
|
return formatDuration(subtitle[key], 3).replace('.', ',');
|
||||||
|
}).join(' --> '),
|
||||||
|
subtitle['text']
|
||||||
|
].join('\r\n')
|
||||||
|
}).join('\r\n\r\n') + '\r\n\r\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
formatString <f> Basic string formatting
|
||||||
|
> formatString('{0}{1}', ['foo', 'bar'])
|
||||||
|
'foobar'
|
||||||
|
> formatString('{a}{b}', {a: 'foo', b: 'bar'})
|
||||||
|
'foobar'
|
||||||
|
> formatString('{a.x}{a.y}', {a: {x: 'foo', y: 'bar'}})
|
||||||
|
'foobar'
|
||||||
|
> formatString('{a\\.b}', {'a.b': 'foobar'})
|
||||||
|
'foobar'
|
||||||
|
> formatString('{1}', ['foobar'])
|
||||||
|
''
|
||||||
|
> formatString('{b}', {a: 'foobar'}, true)
|
||||||
|
'{b}'
|
||||||
|
@*/
|
||||||
|
export function formatString(string, collection, keepUnmatched) {
|
||||||
|
return string.replace(/\{([^}]+)\}/g, function(string, match) {
|
||||||
|
// make sure to not split at escaped dots ('\.')
|
||||||
|
var key,
|
||||||
|
keys = match.replace(/\\\./g, '\n').split('.').map(function(key) {
|
||||||
|
return key.replace(/\n/g, '.');
|
||||||
|
}),
|
||||||
|
value = collection || {};
|
||||||
|
while (keys.length) {
|
||||||
|
key = keys.shift();
|
||||||
|
if (value[key]) {
|
||||||
|
value = value[key];
|
||||||
|
} else {
|
||||||
|
value = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value !== null ? value : keepUnmatched ? '{' + match + '}' : '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
formatUnit <f> Formats a number with a unit
|
||||||
|
> formatUnit(100/3, 'm', 2)
|
||||||
|
'33.33 m'
|
||||||
|
> formatUnit(100/3, '%')
|
||||||
|
'33%'
|
||||||
|
@*/
|
||||||
|
export function formatUnit(number, string, decimals) {
|
||||||
|
return formatNumber(number, decimals)
|
||||||
|
+ (/^[:%]/.test(string) ? '' : ' ') + string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
formatValue <f> Formats a numerical value
|
||||||
|
> formatValue(0, "B")
|
||||||
|
"0 B"
|
||||||
|
> formatValue(123456789, "B")
|
||||||
|
"123.5 MB"
|
||||||
|
> formatValue(1234567890, "B", true)
|
||||||
|
"1.15 GiB"
|
||||||
|
@*/
|
||||||
|
// fixme: is this the best name?
|
||||||
|
export function formatValue(number, string, bin) {
|
||||||
|
var base = bin ? 1024 : 1000,
|
||||||
|
length = PREFIXES.length,
|
||||||
|
ret;
|
||||||
|
forEach(PREFIXES, function(prefix, index) {
|
||||||
|
if (number < Math.pow(base, index + 1) || index == length - 1) {
|
||||||
|
ret = formatNumber(
|
||||||
|
number / Math.pow(base, index), index ? index - 1 : 0
|
||||||
|
) + ' ' + prefix + (prefix && bin ? 'i' : '') + string;
|
||||||
|
return false; // break
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a date string - simplified implementation for now
|
||||||
|
*/
|
||||||
|
export function parseDate(string, utc) {
|
||||||
|
return new Date(string);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export all functions
|
||||||
|
export default {
|
||||||
|
formatArea,
|
||||||
|
formatCount,
|
||||||
|
formatCurrency,
|
||||||
|
formatDate,
|
||||||
|
parseDate,
|
||||||
|
formatDateRange,
|
||||||
|
formatDateRangeDuration,
|
||||||
|
formatDegrees,
|
||||||
|
formatDimensions,
|
||||||
|
formatResolution,
|
||||||
|
formatDuration,
|
||||||
|
formatISBN,
|
||||||
|
formatNumber,
|
||||||
|
formatOrdinal,
|
||||||
|
formatPercent,
|
||||||
|
formatRoman,
|
||||||
|
formatSRT,
|
||||||
|
formatString,
|
||||||
|
formatUnit,
|
||||||
|
formatValue
|
||||||
|
};
|
||||||
7
src/ox/core/Geo.js
Normal file
7
src/ox/core/Geo.js
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
// Geo module - geographic utilities
|
||||||
|
// This will be migrated in a future iteration
|
||||||
|
// For now, we provide basic stubs
|
||||||
|
|
||||||
|
export function parseGeoname() {
|
||||||
|
throw new Error('Geo.parseGeoname not yet migrated to ES modules');
|
||||||
|
}
|
||||||
678
src/ox/core/HTML.js
Normal file
678
src/ox/core/HTML.js
Normal file
|
|
@ -0,0 +1,678 @@
|
||||||
|
import { range, char, random, values, keyOf, forEach, map, splice, contains, isEmpty, clean } from './Array.js';
|
||||||
|
import { isRegExp, isUndefined } from './Type.js';
|
||||||
|
import { escapeRegExp } from './RegExp.js';
|
||||||
|
import { formatString } from './Format.js';
|
||||||
|
import { pad } from './String.js';
|
||||||
|
|
||||||
|
var defaultTags = [
|
||||||
|
// inline formatting
|
||||||
|
{'name': 'b'},
|
||||||
|
{'name': 'bdi'},
|
||||||
|
{'name': 'code'},
|
||||||
|
{'name': 'em'},
|
||||||
|
{'name': 'i'},
|
||||||
|
{'name': 'q'},
|
||||||
|
{'name': 's'},
|
||||||
|
{'name': 'span'},
|
||||||
|
{'name': 'strong'},
|
||||||
|
{'name': 'sub'},
|
||||||
|
{'name': 'sup'},
|
||||||
|
{'name': 'u'},
|
||||||
|
// block formatting
|
||||||
|
{'name': 'blockquote'},
|
||||||
|
{'name': 'cite'},
|
||||||
|
{
|
||||||
|
'name': 'div',
|
||||||
|
'optional': ['style'],
|
||||||
|
'validate': {
|
||||||
|
'style': /^direction: rtl$/
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{'name': 'h1'},
|
||||||
|
{'name': 'h2'},
|
||||||
|
{'name': 'h3'},
|
||||||
|
{'name': 'h4'},
|
||||||
|
{'name': 'h5'},
|
||||||
|
{'name': 'h6'},
|
||||||
|
{'name': 'p'},
|
||||||
|
{'name': 'pre'},
|
||||||
|
// lists
|
||||||
|
{'name': 'li'},
|
||||||
|
{'name': 'ol'},
|
||||||
|
{'name': 'ul'},
|
||||||
|
// definition lists
|
||||||
|
{'name': 'dl'},
|
||||||
|
{'name': 'dt'},
|
||||||
|
{'name': 'dd'},
|
||||||
|
// tables
|
||||||
|
{'name': 'table'},
|
||||||
|
{'name': 'tbody'},
|
||||||
|
{'name': 'td'},
|
||||||
|
{'name': 'tfoot'},
|
||||||
|
{'name': 'th'},
|
||||||
|
{'name': 'thead'},
|
||||||
|
{'name': 'tr'},
|
||||||
|
// other
|
||||||
|
{'name': '[]'},
|
||||||
|
{
|
||||||
|
'name': 'a',
|
||||||
|
'required': ['href'],
|
||||||
|
'optional': ['target'],
|
||||||
|
'validate': {
|
||||||
|
'href': /^((https?:\/\/|\/|mailto:).*?)/,
|
||||||
|
'target': /^_blank$/
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{'name': 'br'},
|
||||||
|
{
|
||||||
|
'name': 'iframe',
|
||||||
|
'optional': ['width', 'height'],
|
||||||
|
'required': ['src'],
|
||||||
|
'validate': {
|
||||||
|
'width': /^\d+$/,
|
||||||
|
'height': /^\d+$/,
|
||||||
|
'src': /^((https?:\/\/|\/).*?)/
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'img',
|
||||||
|
'optional': ['width', 'height'],
|
||||||
|
'required': ['src'],
|
||||||
|
'validate': {
|
||||||
|
'width': /^\d+$/,
|
||||||
|
'height': /^\d+$/,
|
||||||
|
'src': /^((https?:\/\/|\/).*?)/
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{'name': 'figure'},
|
||||||
|
{'name': 'figcaption'}
|
||||||
|
],
|
||||||
|
htmlEntities = {
|
||||||
|
'"': '"', '&': '&', "'": ''', '<': '<', '>': '>'
|
||||||
|
},
|
||||||
|
regexp = {
|
||||||
|
entity: /&[^\s]+?;/g,
|
||||||
|
html: /[<&]/,
|
||||||
|
tag: new RegExp('<\\/?(' + [
|
||||||
|
'a', 'b', 'br', 'code', 'i', 's', 'span', 'u'
|
||||||
|
].join('|') + ')\\/?>', 'gi')
|
||||||
|
},
|
||||||
|
salt = range(2).map(function(){
|
||||||
|
return range(16).map(function() {
|
||||||
|
return char(65 + random(26));
|
||||||
|
}).join('');
|
||||||
|
});
|
||||||
|
|
||||||
|
function addLinksInternal(string, obfuscate) {
|
||||||
|
return string
|
||||||
|
.replace(
|
||||||
|
/\b((https?:\/\/|www\.).+?)([.,:;!?)\]]*?(\s|$))/gi,
|
||||||
|
function(match, url, prefix, end) {
|
||||||
|
prefix = prefix.toLowerCase() == 'www.' ? 'http://' : '';
|
||||||
|
return formatString(
|
||||||
|
'<a href="{prefix}{url}">{url}</a>{end}',
|
||||||
|
{end: end, prefix: prefix, url: url}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
/\b([0-9A-Z.+\-_]+@(?:[0-9A-Z\-]+\.)+[A-Z]{2,6})\b/gi,
|
||||||
|
obfuscate ? function(match, mail) {
|
||||||
|
return encodeEmailAddress(mail);
|
||||||
|
} : '<a href="mailto:$1">$1</a>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeHTMLEntitiesInternal(string) {
|
||||||
|
return string
|
||||||
|
.replace(
|
||||||
|
new RegExp('(' + values(htmlEntities).join('|') + ')', 'g'),
|
||||||
|
function(match) {
|
||||||
|
return keyOf(htmlEntities, match);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
/&#([0-9A-FX]+);/gi,
|
||||||
|
function(match, code) {
|
||||||
|
return char(
|
||||||
|
/^X/i.test(code)
|
||||||
|
? parseInt(code.slice(1), 16)
|
||||||
|
: parseInt(code, 10)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Splits a string into text (even indices) and tags (odd indices), ignoring
|
||||||
|
// tags with starting positions that are included in the ignore array
|
||||||
|
function splitHTMLTags(string, ignore) {
|
||||||
|
var isTag = false, ret = [''];
|
||||||
|
ignore = ignore || [];
|
||||||
|
forEach(string, function(char, i) {
|
||||||
|
if (!isTag && char == '<' && ignore.indexOf(i) == -1) {
|
||||||
|
isTag = true;
|
||||||
|
ret.push('');
|
||||||
|
}
|
||||||
|
ret[ret.length - 1] += char;
|
||||||
|
if (isTag && char == '>') {
|
||||||
|
isTag = false;
|
||||||
|
ret.push('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.addLinks <f> Takes a string and adds links for e-mail addresses and URLs
|
||||||
|
(string[, isHTML]) -> <s> Formatted string
|
||||||
|
string <s> String
|
||||||
|
isHTML <b|false> If true, ignore matches in tags or enclosed by links
|
||||||
|
> Ox.addLinks('foo bar <foo@bar.com>')
|
||||||
|
'foo bar <<a href="mailto:foo@bar.com">foo@bar.com</a>>'
|
||||||
|
> Ox.addLinks('www.foo.com/bar#baz, etc.')
|
||||||
|
'<a href="http://www.foo.com/bar#baz">www.foo.com/bar#baz</a>, etc.'
|
||||||
|
> Ox.addLinks('<a href="http://www.foo.com">www.foo.com</a>', true)
|
||||||
|
'<a href="http://www.foo.com">www.foo.com</a>'
|
||||||
|
@*/
|
||||||
|
export function addLinks(string, isHTML) {
|
||||||
|
var isLink = false;
|
||||||
|
return isHTML
|
||||||
|
? splitHTMLTags(string).map(function(string, i) {
|
||||||
|
var isTag = i % 2;
|
||||||
|
if (isTag) {
|
||||||
|
if (/^<a/.test(string)) {
|
||||||
|
isLink = true;
|
||||||
|
} else if (/^<\/a/.test(string)) {
|
||||||
|
isLink = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isTag || isLink ? string : addLinksInternal(string);
|
||||||
|
}).join('')
|
||||||
|
: normalizeHTML(addLinksInternal(string));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.encodeEmailAddress <f> Returns obfuscated mailto: link
|
||||||
|
> Ox.encodeEmailAddress('mailto:foo@bar.com').indexOf(':') > -1
|
||||||
|
true
|
||||||
|
@*/
|
||||||
|
export function encodeEmailAddress(address, text) {
|
||||||
|
var parts = ['mailto:' + address, text || address].map(function(part) {
|
||||||
|
return map(part, function(char) {
|
||||||
|
var code = char.charCodeAt(0);
|
||||||
|
return char == ':' ? ':'
|
||||||
|
: '&#'
|
||||||
|
+ (Math.random() < 0.5 ? code : 'x' + code.toString(16))
|
||||||
|
+ ';';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return '<a href="' + parts[0] + '">' + parts[1] + '</a>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.encodeHTMLEntities <f> Encodes HTML entities
|
||||||
|
(string[, encodeAll]) -> <s> String
|
||||||
|
string <s> String
|
||||||
|
encodeAll <b|false> If true, encode characters > 127 as numeric entities
|
||||||
|
> Ox.encodeHTMLEntities('<\'&"> äbçdê')
|
||||||
|
'<'&"> äbçdê'
|
||||||
|
> Ox.encodeHTMLEntities('<\'&"> äbçdê', true)
|
||||||
|
'<'&"> äbçdê'
|
||||||
|
@*/
|
||||||
|
export function encodeHTMLEntities(string, encodeAll) {
|
||||||
|
return map(String(string), function(char) {
|
||||||
|
var code = char.charCodeAt(0);
|
||||||
|
if (code < 128) {
|
||||||
|
char = char in htmlEntities ? htmlEntities[char] : char;
|
||||||
|
} else if (encodeAll) {
|
||||||
|
char = '&#x'
|
||||||
|
+ pad(code.toString(16).toUpperCase(), 'left', 4, '0')
|
||||||
|
+ ';';
|
||||||
|
}
|
||||||
|
return char;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.decodeHTMLEntities <f> Decodes HTML entities
|
||||||
|
(string[, decodeAll]) -> <s> String
|
||||||
|
string <s> String
|
||||||
|
decodeAll <b|false> If true, decode named entities for characters > 127
|
||||||
|
Note that `decodeAll` relies on `Ox.normalizeHTML`, which uses the
|
||||||
|
DOM and may transform the string
|
||||||
|
> Ox.decodeHTMLEntities('<'&">')
|
||||||
|
'<\'&">'
|
||||||
|
> Ox.decodeHTMLEntities('<'&">')
|
||||||
|
'<\'&">'
|
||||||
|
> Ox.decodeHTMLEntities('äbçdê')
|
||||||
|
'äbçdê'
|
||||||
|
> Ox.decodeHTMLEntities('äbçdê')
|
||||||
|
'äbçdê'
|
||||||
|
> Ox.decodeHTMLEntities('äbçdê', true)
|
||||||
|
'äbçdê'
|
||||||
|
> Ox.decodeHTMLEntities('<b>β')
|
||||||
|
'<b>β'
|
||||||
|
> Ox.decodeHTMLEntities('<b>β', true)
|
||||||
|
'<b>β</b>'
|
||||||
|
> Ox.decodeHTMLEntities('<b>')
|
||||||
|
'<b>'
|
||||||
|
@*/
|
||||||
|
export function decodeHTMLEntities(string, decodeAll) {
|
||||||
|
return decodeAll
|
||||||
|
? decodeHTMLEntities(normalizeHTML(string))
|
||||||
|
: decodeHTMLEntitiesInternal(string);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.highlight <f> Highlight matches in string
|
||||||
|
(string, query, classname[, isHTML]) -> Output string
|
||||||
|
string <s> Input string
|
||||||
|
query <r|s> Case-insentitive query string, or regular expression
|
||||||
|
classname <s> Class name for matches
|
||||||
|
isHTML <b|false> If true, the input string is treated as HTML
|
||||||
|
> Ox.highlight('<foo><bar>', 'foo', 'c')
|
||||||
|
'<<span class="c">foo</span>><bar>'
|
||||||
|
> Ox.highlight('&', '&', 'c')
|
||||||
|
'<span class="c">&amp;</span>'
|
||||||
|
> Ox.highlight('&', '&', 'c')
|
||||||
|
'&'
|
||||||
|
> Ox.highlight('<foo> <foo>', '<foo>', 'c', true)
|
||||||
|
'<span class="c"><foo></span> <span class="c"><foo></span>'
|
||||||
|
> Ox.highlight('<span class="name">name</span>', 'name', 'c', true)
|
||||||
|
'<span class="name"><span class="c">name</span></span>'
|
||||||
|
> Ox.highlight('amp & amp', 'amp', 'c', true)
|
||||||
|
'<span class="c">amp</span> & <span class="c">amp</span>'
|
||||||
|
> Ox.highlight('amp & amp', 'amp & amp', 'c', true)
|
||||||
|
'<span class="c">amp & amp</span>'
|
||||||
|
> Ox.highlight('<b><b></b>', '<b>', 'c', true)
|
||||||
|
'<span class="c"><b><b></b></span>'
|
||||||
|
> Ox.highlight('<b><b></b>', '<b>', 'c', true)
|
||||||
|
'<b><b></b>'
|
||||||
|
> Ox.highlight('foo<b>bar</b>baz', 'foobar', 'c', true)
|
||||||
|
'<span class="c">foo<b>bar</b></span>baz'
|
||||||
|
> Ox.highlight('foo<p>bar</p>baz', 'foobar', 'c', true)
|
||||||
|
'foo<p>bar</p>baz'
|
||||||
|
> Ox.highlight('foo <br/>bar baz', 'foo bar', 'c', true)
|
||||||
|
'<span class="c">foo <br>bar</span> baz'
|
||||||
|
@*/
|
||||||
|
export function highlight(string, query, classname, isHTML) {
|
||||||
|
if (!query) {
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
var cursor = 0,
|
||||||
|
entities = [],
|
||||||
|
matches = [],
|
||||||
|
offset = 0,
|
||||||
|
re = isRegExp(query) ? query
|
||||||
|
: new RegExp(escapeRegExp(query), 'gi'),
|
||||||
|
span = ['<span class="' + classname + '">', '</span>'],
|
||||||
|
tags = [];
|
||||||
|
function insert(array) {
|
||||||
|
// for each replacement
|
||||||
|
array.forEach(function(v) {
|
||||||
|
// replace the modified value with the original value
|
||||||
|
string = splice(string, v.position, v.length, v.value);
|
||||||
|
// for each match
|
||||||
|
matches.forEach(function(match) {
|
||||||
|
if (v.position < match.position) {
|
||||||
|
// replacement is before match, update match position
|
||||||
|
match.position += v.value.length - v.length;
|
||||||
|
} else if (
|
||||||
|
v.position < match.position + match.value.length
|
||||||
|
) {
|
||||||
|
// replacement is inside match, update match value
|
||||||
|
match.value = splice(
|
||||||
|
match.value, v.position - match.position, v.length,
|
||||||
|
v.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (isHTML && regexp.html.test(string)) {
|
||||||
|
string = string // Ox.normalizeHTML(string)
|
||||||
|
// remove inline tags
|
||||||
|
.replace(regexp.tag, function(value, tag, position) {
|
||||||
|
tags.push({
|
||||||
|
length: 0, position: position, value: value
|
||||||
|
});
|
||||||
|
return '';
|
||||||
|
})
|
||||||
|
// decode html entities
|
||||||
|
.replace(regexp.entity, function(value, position) {
|
||||||
|
var ret = decodeHTMLEntitiesInternal(value, true);
|
||||||
|
entities.push({
|
||||||
|
length: ret.length, position: position, value: value
|
||||||
|
});
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
|
// if decoding entities has created new tags, ignore them
|
||||||
|
splitHTMLTags(string, entities.map(function(entity) {
|
||||||
|
var ret = entity.position + offset;
|
||||||
|
offset += entity.length - entity.value.length;
|
||||||
|
return ret;
|
||||||
|
})).forEach(function(v, i) {
|
||||||
|
if (i % 2 == 0) {
|
||||||
|
// outside tags, find matches and save position and value
|
||||||
|
v.replace(re, function(value, position) {
|
||||||
|
matches.push(
|
||||||
|
{position: cursor + position, value: value}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
cursor += v.length;
|
||||||
|
});
|
||||||
|
insert(entities);
|
||||||
|
insert(tags);
|
||||||
|
// for each match (in reverse order, so that positions are correct)
|
||||||
|
matches.reverse().forEach(function(match) {
|
||||||
|
// wrap it in a span
|
||||||
|
string = splice(
|
||||||
|
string, match.position, match.value.length,
|
||||||
|
span.join(match.value)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
// we may have enclosed single opening or closing tags in a span
|
||||||
|
if (matches.length && tags.length) {
|
||||||
|
string = normalizeHTML(string);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
string = encodeHTMLEntities(
|
||||||
|
string.replace(re, function(value) {
|
||||||
|
matches.push(span.join(encodeHTMLEntities(value)));
|
||||||
|
return salt.join(matches.length - 1);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
matches.forEach(function(match, i) {
|
||||||
|
string = string.replace(new RegExp(salt.join(i)), match);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.normalizeHTML <f> Normalize HTML (using the DOM)
|
||||||
|
> Ox.normalizeHTML('<b>foo')
|
||||||
|
'<b>foo</b>'
|
||||||
|
> Ox.normalizeHTML('<b>foo</b></b>')
|
||||||
|
'<b>foo</b>'
|
||||||
|
> Ox.normalizeHTML('<'&"> äbçdê')
|
||||||
|
'<\'&"> äbçdê'
|
||||||
|
@*/
|
||||||
|
export function normalizeHTML(html) {
|
||||||
|
if (!regexp.html.test(html)) {
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
// Simple implementation for Node.js environments
|
||||||
|
// In browser environments, this would use DOM manipulation
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
var div = document.createElement('div');
|
||||||
|
div.innerHTML = html;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.parseMarkdown <f> Parses (a tiny subset of) Markdown.
|
||||||
|
Supports `*emphasis*`, `_emphasis_`, `**strong**`, `__strong__`,
|
||||||
|
`` `code` ``, ``` ``code with backtick (`)`` ```,
|
||||||
|
```` ```classname\ngithub-style\ncode blocks\n``` ````,
|
||||||
|
`<mail@example.com>`, `<http://example.com>` and
|
||||||
|
`[text](http://example.com "title")`.
|
||||||
|
> Ox.parseMarkdown('*foo* **bar** `baz` ``back`tick``')
|
||||||
|
'<em>foo</em> <strong>bar</strong> <code>baz</code> <code>back`tick</code>'
|
||||||
|
> Ox.parseMarkdown('foo\n\nbar\n\nbaz')
|
||||||
|
'foo<br><br>bar<br><br>baz'
|
||||||
|
> Ox.parseMarkdown('```foo\n\nbar\n\nbaz\n```')
|
||||||
|
'<pre><code class="foo">bar\n\nbaz\n</code></pre>'
|
||||||
|
> Ox.parseMarkdown('<http://example.com>')
|
||||||
|
'<a href="http://example.com">http://example.com</a>'
|
||||||
|
> Ox.parseMarkdown('`<http://example.com>`')
|
||||||
|
'<code><http://example.com></code>'
|
||||||
|
> Ox.parseMarkdown('[example](http://example.com "example.com")')
|
||||||
|
'<a href="http://example.com" title="example.com">example</a>'
|
||||||
|
> Ox.parseMarkdown('[example](http://example.com?foo=bar&bar=baz)')
|
||||||
|
'<a href="http://example.com?foo=bar&bar=baz">example</a>'
|
||||||
|
> Ox(Ox.parseMarkdown('<mail@example.com>')).startsWith('<a href="')
|
||||||
|
true
|
||||||
|
> Ox(Ox.parseMarkdown('<mail@example.com>')).endsWith('</a>')
|
||||||
|
true
|
||||||
|
> Ox(Ox.parseMarkdown('<mail@example.com>')).count(':')
|
||||||
|
1
|
||||||
|
> Ox(Ox.parseMarkdown('<mail@example.com>')).decodeHTMLEntities()
|
||||||
|
'<a href="mailto:mail@example.com">mail@example.com</a>'
|
||||||
|
*/
|
||||||
|
export function parseMarkdown(string) {
|
||||||
|
// see https://github.com/coreyti/showdown/blob/master/src/showdown.js
|
||||||
|
var array = [];
|
||||||
|
return string.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
||||||
|
.replace(
|
||||||
|
/(?:^|\n)```(.*)\n([^`]+)\n```/g,
|
||||||
|
function(match, classname, code) {
|
||||||
|
array.push(
|
||||||
|
'<pre><code'
|
||||||
|
+ (classname ? ' class="' + classname + '"' : '') + '>'
|
||||||
|
+ code.trim().replace(/</g, '<') + '\n</code></pre>'
|
||||||
|
);
|
||||||
|
return salt.join(array.length - 1);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm,
|
||||||
|
function(match, prev, backticks, code, next) {
|
||||||
|
array.push(
|
||||||
|
prev + '<code>'
|
||||||
|
+ code.trim().replace(/</g, '<') + '</code>'
|
||||||
|
);
|
||||||
|
return salt.join(array.length - 1);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
/(\*\*|__)(?=\S)([^\r]*?\S[*_]*)\1/g,
|
||||||
|
'<strong>$2</strong>'
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
/(\*|_)(?=\S)([^\r]*?\S)\1/g,
|
||||||
|
'<em>$2</em>'
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()<?(.*?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,
|
||||||
|
function(match, all, text, id, url, rest, quote, title) {
|
||||||
|
return '<a href="' + encodeHTMLEntities(url) + '"' + (
|
||||||
|
title ? ' title="' + encodeHTMLEntities(title) + '"' : ''
|
||||||
|
) + '>' + text + '</a>';
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
/<((https?|ftp|dict):[^'">\s]+)>/gi,
|
||||||
|
'<a href=\"$1\">$1</a>'
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
/<(?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi,
|
||||||
|
function(match, mail) {
|
||||||
|
return encodeEmailAddress(mail);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.replace(/\n\n/g, '<br><br>')
|
||||||
|
.replace(
|
||||||
|
new RegExp(salt.join('(\\d+)'), 'g'),
|
||||||
|
function(match, index) {
|
||||||
|
return array[parseInt(index)];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.sanitizeHTML <f> Takes untrusted HTML and returns something trustworthy
|
||||||
|
> Ox.sanitizeHTML('http://foo.com, ...')
|
||||||
|
'<a href="http://foo.com">http://foo.com</a>, ...'
|
||||||
|
> Ox.sanitizeHTML('http://foo.com/foo?bar&baz, ...')
|
||||||
|
'<a href="http://foo.com/foo?bar&baz">http://foo.com/foo?bar&baz</a>, ...'
|
||||||
|
> Ox.sanitizeHTML('(see: www.foo.com)')
|
||||||
|
'(see: <a href="http://www.foo.com">www.foo.com</a>)'
|
||||||
|
> Ox.sanitizeHTML('foo@bar.com')
|
||||||
|
'<a href="mailto:foo@bar.com">foo@bar.com</a>'
|
||||||
|
> Ox.sanitizeHTML('<a href="mailto:foo@bar.com">foo</a>')
|
||||||
|
'<a href="mailto:foo@bar.com">foo</a>'
|
||||||
|
> Ox.sanitizeHTML('<a href="http://foo.com">foo</a>')
|
||||||
|
'<a href="http://foo.com">foo</a>'
|
||||||
|
> Ox.sanitizeHTML('<a href="http://www.foo.com/">http://www.foo.com/</a>')
|
||||||
|
'<a href="http://www.foo.com/">http://www.foo.com/</a>'
|
||||||
|
> Ox.sanitizeHTML('<a href="http://foo.com" onclick="alert()">foo</a>')
|
||||||
|
'<a href="http://foo.com">foo</a>'
|
||||||
|
> Ox.sanitizeHTML('<a href="http://foo.com" target="_blank">foo</a>')
|
||||||
|
'<a href="http://foo.com" target="_blank">foo</a>'
|
||||||
|
> Ox.sanitizeHTML('<a href="javascript:alert()">foo</a>')
|
||||||
|
'<a href="javascript:alert()">foo</a>'
|
||||||
|
> Ox.sanitizeHTML('<a href="foo">foo</a>')
|
||||||
|
'<a href="foo">foo</a>'
|
||||||
|
> Ox.sanitizeHTML('<a href="/foo">foo</a>')
|
||||||
|
'<a href="/foo">foo</a>'
|
||||||
|
> Ox.sanitizeHTML('<a href="/">foo</a>')
|
||||||
|
'<a href="/">foo</a>'
|
||||||
|
> Ox.sanitizeHTML('[http://foo.com foo]')
|
||||||
|
'<a href="http://foo.com">foo</a>'
|
||||||
|
> Ox.sanitizeHTML('<div style="direction: rtl">foo</div>')
|
||||||
|
'<div style="direction: rtl">foo</div>'
|
||||||
|
> Ox.sanitizeHTML('<script>alert()</script>')
|
||||||
|
'<script>alert()</script>'
|
||||||
|
> Ox.sanitizeHTML('\'foo\' < \'bar\' && "foo" > "bar"')
|
||||||
|
'\'foo\' < \'bar\' && "foo" > "bar"'
|
||||||
|
> Ox.sanitizeHTML('<b>foo')
|
||||||
|
'<b>foo</b>'
|
||||||
|
> Ox.sanitizeHTML('<b>foo</b></b>')
|
||||||
|
'<b>foo</b>'
|
||||||
|
> Ox.sanitizeHTML('&&')
|
||||||
|
'&&'
|
||||||
|
> Ox.sanitizeHTML('<http://foo.com>')
|
||||||
|
'<<a href="http://foo.com">http://foo.com</a>>'
|
||||||
|
> Ox.sanitizeHTML('<foo value="http://foo.com"></foo>')
|
||||||
|
'"<foo value="http://foo.com"></foo>"'
|
||||||
|
@*/
|
||||||
|
export function sanitizeHTML(html, tags, globalAttributes) {
|
||||||
|
|
||||||
|
tags = tags || defaultTags;
|
||||||
|
globalAttributes = globalAttributes || [];
|
||||||
|
|
||||||
|
var escaped = {},
|
||||||
|
level = 0,
|
||||||
|
matches = [],
|
||||||
|
selfClosingTags = ['img', 'br'],
|
||||||
|
validAttributes = {}, requiredAttributes = {}, validate = {},
|
||||||
|
validTags = tags.map(function(tag) {
|
||||||
|
validAttributes[tag.name] = globalAttributes
|
||||||
|
.concat(tag.required || [])
|
||||||
|
.concat(tag.optional || []);
|
||||||
|
requiredAttributes[tag.name] = tag.required || [];
|
||||||
|
validate[tag.name] = tag.validate || {};
|
||||||
|
return tag.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
// html = clean(html); fixme: can this be a parameter?
|
||||||
|
if (contains(validTags, '[]')) {
|
||||||
|
html = html.replace(
|
||||||
|
/\[((\/|https?:\/\/|mailto:).+?) (.+?)\]/gi,
|
||||||
|
'<a href="$1">$3</a>'
|
||||||
|
);
|
||||||
|
validTags = validTags.filter(function(tag) {
|
||||||
|
return tag != '[]';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
html = splitHTMLTags(html).map(function(string, i) {
|
||||||
|
|
||||||
|
var attrs = {},
|
||||||
|
attrMatch,
|
||||||
|
attrRegexp = /([^=\ ]+)="([^"]+)"/g,
|
||||||
|
attrString,
|
||||||
|
isClosing,
|
||||||
|
isTag = i % 2,
|
||||||
|
isValid = true,
|
||||||
|
tag,
|
||||||
|
tagMatch,
|
||||||
|
tagRegexp = /<(\/)?([^\ \/]+)(.*?)(\/)?>/g;
|
||||||
|
|
||||||
|
if (isTag) {
|
||||||
|
tagMatch = tagRegexp.exec(string);
|
||||||
|
if (tagMatch) {
|
||||||
|
isClosing = !isUndefined(tagMatch[1]);
|
||||||
|
tag = tagMatch[2];
|
||||||
|
attrString = tagMatch[3].trim();
|
||||||
|
while (attrMatch = attrRegexp.exec(attrString)) {
|
||||||
|
if (
|
||||||
|
validAttributes[tag]
|
||||||
|
&& contains(validAttributes[tag], attrMatch[1])
|
||||||
|
) {
|
||||||
|
attrs[attrMatch[1]] = attrMatch[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isClosing && !contains(selfClosingTags, tag)) {
|
||||||
|
level++;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!contains(validTags, tag)
|
||||||
|
|| (attrString.length && isEmpty(attrs))
|
||||||
|
) {
|
||||||
|
isValid = false;
|
||||||
|
} else if (!isClosing && requiredAttributes[tag]) {
|
||||||
|
requiredAttributes[tag].forEach(function(attr) {
|
||||||
|
if (isUndefined(attrs[attr])) {
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (isValid && !isEmpty(attrs)) {
|
||||||
|
forEach(attrs, function(value, key) {
|
||||||
|
if (
|
||||||
|
!isUndefined(validate[tag][key])
|
||||||
|
&& !validate[tag][key].exec(value)
|
||||||
|
) {
|
||||||
|
isValid = false;
|
||||||
|
return false; // break
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (isValid && isClosing) {
|
||||||
|
isValid = !escaped[level];
|
||||||
|
} else {
|
||||||
|
escaped[level] = !isValid;
|
||||||
|
}
|
||||||
|
if (isClosing) {
|
||||||
|
level--;
|
||||||
|
}
|
||||||
|
if (isValid) {
|
||||||
|
return '<'
|
||||||
|
+ (isClosing ? '/' : '')
|
||||||
|
+ tag
|
||||||
|
+ (!isClosing && !isEmpty(attrs)
|
||||||
|
? ' ' + values(map(attrs, function(value, key) {
|
||||||
|
return key + '="' + value + '"';
|
||||||
|
})).join(' ')
|
||||||
|
: '')
|
||||||
|
+ '>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return encodeHTMLEntities(decodeHTMLEntitiesInternal(string));
|
||||||
|
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
//FIXME: dont add links to urls inside of escaped tags
|
||||||
|
html = addLinks(html, true);
|
||||||
|
html = html.replace(/\n\n/g, '<br/><br/>');
|
||||||
|
// Close extra opening and remove extra closing tags.
|
||||||
|
// Note: this converts ''' to "'" and '"' to '"'
|
||||||
|
return normalizeHTML(html);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.stripTags <f> Strips HTML tags from a string
|
||||||
|
> Ox.stripTags('f<span>o</span>o')
|
||||||
|
'foo'
|
||||||
|
@*/
|
||||||
|
export function stripTags(string) {
|
||||||
|
return string.replace(/<.*?>/g, '');
|
||||||
|
}
|
||||||
229
src/ox/core/Hash.js
Normal file
229
src/ox/core/Hash.js
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
import { pad } from './String.js';
|
||||||
|
import { encodeUTF8 } from './Encoding.js';
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.oshash <f> Calculates oshash for a given file or blob object. Async.
|
||||||
|
@*/
|
||||||
|
export function oshash(file, callback) {
|
||||||
|
|
||||||
|
// Needs to go via string to work for files > 2GB
|
||||||
|
var hash = fromString(file.size.toString());
|
||||||
|
|
||||||
|
read(0);
|
||||||
|
|
||||||
|
function add(A, B) {
|
||||||
|
var a, b, c, d;
|
||||||
|
d = A[3] + B[3];
|
||||||
|
c = A[2] + B[2] + (d >> 16);
|
||||||
|
d &= 0xffff;
|
||||||
|
b = A[1] + B[1] + (c >> 16);
|
||||||
|
c &= 0xffff;
|
||||||
|
a = A[0] + B[0] + (b >> 16);
|
||||||
|
b &= 0xffff;
|
||||||
|
// Cut off overflow
|
||||||
|
a &= 0xffff;
|
||||||
|
return [a, b, c, d];
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromData(s, offset) {
|
||||||
|
offset = offset || 0;
|
||||||
|
return [
|
||||||
|
s.charCodeAt(offset + 6) + (s.charCodeAt(offset + 7) << 8),
|
||||||
|
s.charCodeAt(offset + 4) + (s.charCodeAt(offset + 5) << 8),
|
||||||
|
s.charCodeAt(offset + 2) + (s.charCodeAt(offset + 3) << 8),
|
||||||
|
s.charCodeAt(offset + 0) + (s.charCodeAt(offset + 1) << 8)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromString(str) {
|
||||||
|
var base = 10,
|
||||||
|
blen = 1,
|
||||||
|
i,
|
||||||
|
num,
|
||||||
|
pos,
|
||||||
|
r = [0, 0, 0, 0];
|
||||||
|
for (pos = 0; pos < str.length; pos++) {
|
||||||
|
num = parseInt(str.charAt(pos), base);
|
||||||
|
i = 0;
|
||||||
|
do {
|
||||||
|
while (i < blen) {
|
||||||
|
num += r[3 - i] * base;
|
||||||
|
r[3 - i++] = (num & 0xFFFF);
|
||||||
|
num >>>= 16;
|
||||||
|
}
|
||||||
|
if (num) {
|
||||||
|
blen++;
|
||||||
|
}
|
||||||
|
} while (num);
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hex(h) {
|
||||||
|
return (
|
||||||
|
pad(h[0].toString(16), 'left', 4, '0')
|
||||||
|
+ pad(h[1].toString(16), 'left', 4, '0')
|
||||||
|
+ pad(h[2].toString(16), 'left', 4, '0')
|
||||||
|
+ pad(h[3].toString(16), 'left', 4, '0')
|
||||||
|
).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function read(offset, last) {
|
||||||
|
var blob,
|
||||||
|
block = 65536,
|
||||||
|
length = 8,
|
||||||
|
reader = new FileReader();
|
||||||
|
reader.onload = function(data) {
|
||||||
|
var s = data.target.result,
|
||||||
|
s_length = s.length - length,
|
||||||
|
i;
|
||||||
|
for (i = 0; i <= s_length; i += length) {
|
||||||
|
hash = add(hash, fromData(s, i));
|
||||||
|
}
|
||||||
|
if (file.size < block || last) {
|
||||||
|
callback(hex(hash));
|
||||||
|
} else {
|
||||||
|
read(file.size - block, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (file.mozSlice) {
|
||||||
|
blob = file.mozSlice(offset, offset + block);
|
||||||
|
} else if (file.webkitSlice) {
|
||||||
|
blob = file.webkitSlice(offset, offset + block);
|
||||||
|
} else {
|
||||||
|
blob = file.slice(offset, offset + block);
|
||||||
|
}
|
||||||
|
reader.readAsBinaryString(blob);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.SHA1 <f> Calculates SHA1 hash of the given string
|
||||||
|
@*/
|
||||||
|
export function SHA1(msg) {
|
||||||
|
|
||||||
|
function rotate_left(n,s) {
|
||||||
|
var t4 = ( n<<s ) | (n>>>(32-s));
|
||||||
|
return t4;
|
||||||
|
};
|
||||||
|
|
||||||
|
function cvt_hex(val) {
|
||||||
|
var str="";
|
||||||
|
var i;
|
||||||
|
var v;
|
||||||
|
for ( i=7; i>=0; i-- ) {
|
||||||
|
v = (val>>>(i*4))&0x0f;
|
||||||
|
str += v.toString(16);
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
};
|
||||||
|
|
||||||
|
var blockstart;
|
||||||
|
var i, j;
|
||||||
|
var W = new Array(80);
|
||||||
|
var H0 = 0x67452301;
|
||||||
|
var H1 = 0xEFCDAB89;
|
||||||
|
var H2 = 0x98BADCFE;
|
||||||
|
var H3 = 0x10325476;
|
||||||
|
var H4 = 0xC3D2E1F0;
|
||||||
|
var A, B, C, D, E;
|
||||||
|
var temp;
|
||||||
|
|
||||||
|
msg = encodeUTF8(msg);
|
||||||
|
|
||||||
|
var msg_len = msg.length;
|
||||||
|
|
||||||
|
var word_array = new Array();
|
||||||
|
for ( i=0; i<msg_len-3; i+=4 ) {
|
||||||
|
j = msg.charCodeAt(i)<<24 | msg.charCodeAt(i+1)<<16 |
|
||||||
|
msg.charCodeAt(i+2)<<8 | msg.charCodeAt(i+3);
|
||||||
|
word_array.push( j );
|
||||||
|
}
|
||||||
|
|
||||||
|
switch( msg_len % 4 ) {
|
||||||
|
case 0:
|
||||||
|
i = 0x080000000;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
i = msg.charCodeAt(msg_len-1)<<24 | 0x0800000;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
i = msg.charCodeAt(msg_len-2)<<24 | msg.charCodeAt(msg_len-1)<<16 | 0x08000;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
i = msg.charCodeAt(msg_len-3)<<24 | msg.charCodeAt(msg_len-2)<<16 | msg.charCodeAt(msg_len-1)<<8 | 0x80;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
word_array.push( i );
|
||||||
|
|
||||||
|
while( (word_array.length % 16) != 14 ) {
|
||||||
|
word_array.push( 0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
word_array.push( msg_len>>>29 );
|
||||||
|
word_array.push( (msg_len<<3)&0x0ffffffff );
|
||||||
|
|
||||||
|
|
||||||
|
for ( blockstart=0; blockstart<word_array.length; blockstart+=16 ) {
|
||||||
|
|
||||||
|
for ( i=0; i<16; i++ ) {
|
||||||
|
W[i] = word_array[blockstart+i];
|
||||||
|
}
|
||||||
|
for ( i=16; i<=79; i++ ) {
|
||||||
|
W[i] = rotate_left(W[i-3] ^ W[i-8] ^ W[i-14] ^ W[i-16], 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
A = H0;
|
||||||
|
B = H1;
|
||||||
|
C = H2;
|
||||||
|
D = H3;
|
||||||
|
E = H4;
|
||||||
|
|
||||||
|
for ( i= 0; i<=19; i++ ) {
|
||||||
|
temp = (rotate_left(A,5) + ((B&C) | (~B&D)) + E + W[i] + 0x5A827999) & 0x0ffffffff;
|
||||||
|
E = D;
|
||||||
|
D = C;
|
||||||
|
C = rotate_left(B, 30);
|
||||||
|
B = A;
|
||||||
|
A = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
for ( i=20; i<=39; i++ ) {
|
||||||
|
temp = (rotate_left(A,5) + (B ^ C ^ D) + E + W[i] + 0x6ED9EBA1) & 0x0ffffffff;
|
||||||
|
E = D;
|
||||||
|
D = C;
|
||||||
|
C = rotate_left(B, 30);
|
||||||
|
B = A;
|
||||||
|
A = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
for ( i=40; i<=59; i++ ) {
|
||||||
|
temp = (rotate_left(A,5) + ((B&C) | (B&D) | (C&D)) + E + W[i] + 0x8F1BBCDC) & 0x0ffffffff;
|
||||||
|
E = D;
|
||||||
|
D = C;
|
||||||
|
C = rotate_left(B, 30);
|
||||||
|
B = A;
|
||||||
|
A = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
for ( i=60; i<=79; i++ ) {
|
||||||
|
temp = (rotate_left(A,5) + (B ^ C ^ D) + E + W[i] + 0xCA62C1D6) & 0x0ffffffff;
|
||||||
|
E = D;
|
||||||
|
D = C;
|
||||||
|
C = rotate_left(B, 30);
|
||||||
|
B = A;
|
||||||
|
A = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
H0 = (H0 + A) & 0x0ffffffff;
|
||||||
|
H1 = (H1 + B) & 0x0ffffffff;
|
||||||
|
H2 = (H2 + C) & 0x0ffffffff;
|
||||||
|
H3 = (H3 + D) & 0x0ffffffff;
|
||||||
|
H4 = (H4 + E) & 0x0ffffffff;
|
||||||
|
|
||||||
|
}
|
||||||
|
var temp = cvt_hex(H0) + cvt_hex(H1) + cvt_hex(H2) + cvt_hex(H3) + cvt_hex(H4);
|
||||||
|
return temp.toLowerCase();
|
||||||
|
}
|
||||||
15
src/ox/core/JavaScript.js
Normal file
15
src/ox/core/JavaScript.js
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
// JavaScript module - complex documentation parser and test runner
|
||||||
|
// This will be migrated in a future iteration due to its complexity
|
||||||
|
// For now, we provide basic stubs
|
||||||
|
|
||||||
|
export function doc() {
|
||||||
|
throw new Error('JavaScript.doc not yet migrated to ES modules');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function test() {
|
||||||
|
throw new Error('JavaScript.test not yet migrated to ES modules');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tokenize() {
|
||||||
|
throw new Error('JavaScript.tokenize not yet migrated to ES modules');
|
||||||
|
}
|
||||||
13
src/ox/core/RegExp.js
Normal file
13
src/ox/core/RegExp.js
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
/*@
|
||||||
|
Ox.escapeRegExp <f> Escapes a string for use in a regular expression
|
||||||
|
(str) -> <r> Escaped string
|
||||||
|
str <s> String
|
||||||
|
> Ox.escapeRegExp('foo.com/bar?baz')
|
||||||
|
'foo\\.com\\/bar\\?baz'
|
||||||
|
> new RegExp(Ox.escapeRegExp('/\\^$*+?.-|(){}[]')).test('/\\^$*+?.-|(){}[]')
|
||||||
|
true
|
||||||
|
@*/
|
||||||
|
// see https://developer.mozilla.org/en/JavaScript/Guide/Regular_Expressions
|
||||||
|
export function escapeRegExp(string) {
|
||||||
|
return (string + '').replace(/([\/\\^$*+?.\-|(){}[\]])/g, '\\$1');
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { isArray, isNumber, isString, isUndefined } from './Type.js';
|
import { isArray, isNumber, isString, isUndefined } from './Type.js';
|
||||||
import { map } from './Collection.js';
|
import { map, slice } from './Collection.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a string with the first letter capitalized
|
* Returns a string with the first letter capitalized
|
||||||
|
|
@ -242,6 +242,22 @@ export function wordwrap(string, length, newline, balanced) {
|
||||||
return lines.join(newline);
|
return lines.join(newline);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.char <f> Alias for String.fromCharCode
|
||||||
|
@*/
|
||||||
|
export const char = String.fromCharCode;
|
||||||
|
|
||||||
|
/*@
|
||||||
|
Ox.splice <f> `[].splice` for strings, returns a new string
|
||||||
|
> Ox.splice('12xxxxx89', 2, 5, 3, 4, 5, 6, 7)
|
||||||
|
'123456789'
|
||||||
|
@*/
|
||||||
|
export function splice(string, index, remove) {
|
||||||
|
const array = string.split('');
|
||||||
|
Array.prototype.splice.apply(array, slice(arguments, 1));
|
||||||
|
return array.join('');
|
||||||
|
}
|
||||||
|
|
||||||
// Export all functions
|
// Export all functions
|
||||||
export default {
|
export default {
|
||||||
capitalize,
|
capitalize,
|
||||||
|
|
@ -266,5 +282,7 @@ export default {
|
||||||
truncate,
|
truncate,
|
||||||
uppercase,
|
uppercase,
|
||||||
words,
|
words,
|
||||||
wordwrap
|
wordwrap,
|
||||||
|
char,
|
||||||
|
splice
|
||||||
};
|
};
|
||||||
7
src/ox/core/Video.js
Normal file
7
src/ox/core/Video.js
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
// Video module - video manipulation utilities
|
||||||
|
// This will be migrated in a future iteration
|
||||||
|
// For now, we provide basic stubs
|
||||||
|
|
||||||
|
export function parsePath() {
|
||||||
|
throw new Error('Video.parsePath not yet migrated to ES modules');
|
||||||
|
}
|
||||||
57
src/ox/core/stubs.js
Normal file
57
src/ox/core/stubs.js
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
/**
|
||||||
|
* Stub implementations for modules not yet converted
|
||||||
|
* These will be replaced with full implementations
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Format utilities stub
|
||||||
|
export const FormatUtils = {
|
||||||
|
formatNumber: (n) => n.toString(),
|
||||||
|
formatDuration: (ms) => `${ms}ms`,
|
||||||
|
formatBytes: (b) => `${b}B`
|
||||||
|
};
|
||||||
|
|
||||||
|
// Color utilities stub
|
||||||
|
export const ColorUtils = {
|
||||||
|
rgb: (r, g, b) => `rgb(${r}, ${g}, ${b})`,
|
||||||
|
hex: (color) => color
|
||||||
|
};
|
||||||
|
|
||||||
|
// Encoding utilities stub
|
||||||
|
export const EncodingUtils = {
|
||||||
|
encodeBase64: (str) => btoa(str),
|
||||||
|
decodeBase64: (str) => atob(str)
|
||||||
|
};
|
||||||
|
|
||||||
|
// RegExp utilities stub
|
||||||
|
export const RegExpUtils = {
|
||||||
|
escape: (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||||
|
};
|
||||||
|
|
||||||
|
// HTML utilities stub
|
||||||
|
export const HTMLUtils = {
|
||||||
|
encode: (str) => str.replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`),
|
||||||
|
decode: (str) => str
|
||||||
|
};
|
||||||
|
|
||||||
|
// Async utilities stub
|
||||||
|
export const AsyncUtils = {
|
||||||
|
sleep: (ms) => new Promise(resolve => setTimeout(resolve, ms)),
|
||||||
|
series: async (tasks) => {
|
||||||
|
const results = [];
|
||||||
|
for (const task of tasks) {
|
||||||
|
results.push(await task());
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Geo utilities stub (enhance existing)
|
||||||
|
export const GeoUtils = {
|
||||||
|
getDistance: (a, b) => Math.sqrt(Math.pow(b.lat - a.lat, 2) + Math.pow(b.lng - a.lng, 2))
|
||||||
|
};
|
||||||
|
|
||||||
|
// JavaScript utilities stub
|
||||||
|
export const JavaScriptUtils = {
|
||||||
|
minify: (code) => code,
|
||||||
|
tokenize: (code) => []
|
||||||
|
};
|
||||||
|
|
@ -16,18 +16,22 @@ import * as CollectionUtils from './core/Collection.js';
|
||||||
import * as MathUtils from './core/Math.js';
|
import * as MathUtils from './core/Math.js';
|
||||||
import * as ObjectUtils from './core/Object.js';
|
import * as ObjectUtils from './core/Object.js';
|
||||||
import * as DateUtils from './core/Date.js';
|
import * as DateUtils from './core/Date.js';
|
||||||
|
import * as DOMUtils from './core/DOM.js';
|
||||||
|
import * as RequestUtils from './core/Request.js';
|
||||||
|
import * as LocaleUtils from './core/Locale.js';
|
||||||
|
import * as Constants from './core/Constants.js';
|
||||||
|
|
||||||
|
// Import newly converted modules
|
||||||
import * as FormatUtils from './core/Format.js';
|
import * as FormatUtils from './core/Format.js';
|
||||||
import * as ColorUtils from './core/Color.js';
|
import * as ColorUtils from './core/Color.js';
|
||||||
import * as EncodingUtils from './core/Encoding.js';
|
import * as EncodingUtils from './core/Encoding.js';
|
||||||
import * as RegExpUtils from './core/RegExp.js';
|
import * as RegExpUtils from './core/RegExp.js';
|
||||||
import * as HTMLUtils from './core/HTML.js';
|
import * as HTMLUtils from './core/HTML.js';
|
||||||
import * as DOMUtils from './core/DOM.js';
|
|
||||||
import * as RequestUtils from './core/Request.js';
|
|
||||||
import * as AsyncUtils from './core/Async.js';
|
import * as AsyncUtils from './core/Async.js';
|
||||||
|
import * as HashUtils from './core/Hash.js';
|
||||||
import * as GeoUtils from './core/Geo.js';
|
import * as GeoUtils from './core/Geo.js';
|
||||||
import * as JavaScriptUtils from './core/JavaScript.js';
|
import * as JavaScriptUtils from './core/JavaScript.js';
|
||||||
import * as LocaleUtils from './core/Locale.js';
|
import * as VideoUtils from './core/Video.js';
|
||||||
import * as Constants from './core/Constants.js';
|
|
||||||
|
|
||||||
// Create the main Ox object
|
// Create the main Ox object
|
||||||
const Ox = function(value) {
|
const Ox = function(value) {
|
||||||
|
|
@ -50,11 +54,13 @@ Object.assign(Ox,
|
||||||
EncodingUtils,
|
EncodingUtils,
|
||||||
RegExpUtils,
|
RegExpUtils,
|
||||||
HTMLUtils,
|
HTMLUtils,
|
||||||
|
HashUtils,
|
||||||
DOMUtils,
|
DOMUtils,
|
||||||
RequestUtils,
|
RequestUtils,
|
||||||
AsyncUtils,
|
AsyncUtils,
|
||||||
GeoUtils,
|
GeoUtils,
|
||||||
JavaScriptUtils,
|
JavaScriptUtils,
|
||||||
|
VideoUtils,
|
||||||
LocaleUtils,
|
LocaleUtils,
|
||||||
Constants
|
Constants
|
||||||
);
|
);
|
||||||
|
|
@ -85,11 +91,13 @@ export {
|
||||||
EncodingUtils,
|
EncodingUtils,
|
||||||
RegExpUtils,
|
RegExpUtils,
|
||||||
HTMLUtils,
|
HTMLUtils,
|
||||||
|
HashUtils,
|
||||||
DOMUtils,
|
DOMUtils,
|
||||||
RequestUtils,
|
RequestUtils,
|
||||||
AsyncUtils,
|
AsyncUtils,
|
||||||
GeoUtils,
|
GeoUtils,
|
||||||
JavaScriptUtils,
|
JavaScriptUtils,
|
||||||
|
VideoUtils,
|
||||||
LocaleUtils,
|
LocaleUtils,
|
||||||
Constants
|
Constants
|
||||||
};
|
};
|
||||||
|
|
|
||||||
22
test/test-setup.js
Normal file
22
test/test-setup.js
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
/**
|
||||||
|
* Test setup for running extracted OxJS inline tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Load OxJS ES modules
|
||||||
|
import Ox from '../src/ox/index.js';
|
||||||
|
|
||||||
|
// Helper function to evaluate test statements in context
|
||||||
|
global.evaluateInContext = async function(statement) {
|
||||||
|
try {
|
||||||
|
// This will need to be enhanced to handle async tests
|
||||||
|
// For now, we'll use eval which isn't ideal but matches the original test system
|
||||||
|
return eval(statement);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error evaluating:', statement, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make Ox available globally for tests
|
||||||
|
global.Ox = Ox;
|
||||||
|
console.log('Test environment setup complete');
|
||||||
41
vite.config.build.js
Normal file
41
vite.config.build.js
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: resolve(__dirname, 'src/ox/index.js'),
|
||||||
|
name: 'Ox',
|
||||||
|
formats: ['es', 'umd'],
|
||||||
|
fileName: (format) => {
|
||||||
|
if (format === 'es') return 'ox.esm.js';
|
||||||
|
if (format === 'umd') return 'ox.umd.js';
|
||||||
|
return `ox.${format}.js`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
globals: {
|
||||||
|
// Any external dependencies would go here
|
||||||
|
},
|
||||||
|
// Keep all exports at top level
|
||||||
|
preserveModules: false,
|
||||||
|
// Ensure compatibility with older environments
|
||||||
|
generatedCode: {
|
||||||
|
constBindings: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sourcemap: true,
|
||||||
|
minify: false, // We'll minify separately for min/Ox.js
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: false
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': resolve(__dirname, './src'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -5,7 +5,7 @@ export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
globals: true,
|
globals: true,
|
||||||
environment: 'node', // Use node for now to avoid jsdom issues
|
environment: 'node', // Use node for now to avoid jsdom issues
|
||||||
setupFiles: './test/setup.js'
|
setupFiles: './test/test-setup.js'
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue