als-layout 7.0.1 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.mjs +89 -81
- package/layout.js +89 -81
- package/package.json +15 -3
- package/readme.md +186 -163
- package/src/layout.js +93 -85
- package/build.js +0 -8
- package/tests/constructor.test.js +0 -28
- package/tests/description.test.js +0 -33
- package/tests/favicon.test.js +0 -36
- package/tests/image.test.js +0 -25
- package/tests/integrative.test.js +0 -83
- package/tests/keywords.test.js +0 -47
- package/tests/link.test.js +0 -33
- package/tests/script.test.js +0 -42
- package/tests/style.test.js +0 -34
- package/tests/url.test.js +0 -42
- package/tests/viewport.test.js +0 -41
package/readme.md
CHANGED
|
@@ -1,163 +1,186 @@
|
|
|
1
|
-
# als-layout
|
|
2
|
-
|
|
3
|
-
`als-layout` is a
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
###
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
.
|
|
69
|
-
.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
- `
|
|
146
|
-
- `
|
|
147
|
-
- `
|
|
148
|
-
- `
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
- `
|
|
152
|
-
- `
|
|
153
|
-
- `
|
|
154
|
-
- `
|
|
155
|
-
- `
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
-
|
|
161
|
-
-
|
|
162
|
-
-
|
|
163
|
-
|
|
1
|
+
# als-layout
|
|
2
|
+
|
|
3
|
+
`als-layout` is a small, dependency‑light library for building and manipulating an HTML
|
|
4
|
+
document in memory — title, meta tags, Open Graph / Twitter cards, styles, scripts, links,
|
|
5
|
+
favicon and viewport — with a fluent, chainable API. It works the same in Node (SSR /
|
|
6
|
+
static generation) and in the browser.
|
|
7
|
+
|
|
8
|
+
It is built on top of [`als-document`](https://www.npmjs.com/package/als-document) and is a
|
|
9
|
+
pure **document** builder: it has no global state and no site‑level concerns (collections,
|
|
10
|
+
sitemaps, robots). For multi‑page sites built on top of it, see
|
|
11
|
+
[`als-site`](https://www.npmjs.com/package/als-site).
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## What's new in 7.1
|
|
16
|
+
|
|
17
|
+
- **`toDocument()`** — render a layout into the live DOM on the client (no‑op in Node).
|
|
18
|
+
- **`urlObj`** — `url()` now exposes the parsed `URL`.
|
|
19
|
+
- **Packaging** — ESM resolves to the named `Layout` export, CommonJS to the class (`exports` map).
|
|
20
|
+
- **Fix** — `twitter:description` uses `name=` (was `property=`), per the Twitter Cards spec.
|
|
21
|
+
- **Fix** — `clone` preserves the document URL/host.
|
|
22
|
+
- **Change** — `url()` keeps the canonical trailing slash (`URL.href`) and stores `urlObj`.
|
|
23
|
+
|
|
24
|
+
See the full [Changelog](#changelog) below.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm i als-layout
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### ESM (named export)
|
|
35
|
+
```js
|
|
36
|
+
import { Layout } from 'als-layout'
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Node (CommonJS)
|
|
40
|
+
```js
|
|
41
|
+
const Layout = require('als-layout')
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Browser bundle (global `Layout`, `als-document` inlined)
|
|
45
|
+
```html
|
|
46
|
+
<script src="layout.js"></script>
|
|
47
|
+
<script>const layout = new Layout()</script>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Quick start
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
import { Layout } from 'als-layout'
|
|
56
|
+
|
|
57
|
+
const layout = new Layout(undefined, 'https://example.com')
|
|
58
|
+
.lang('en')
|
|
59
|
+
.viewport() // width=device-width, initial-scale=1.0
|
|
60
|
+
.title('Home') // <title> + og:title
|
|
61
|
+
.description('A cool site') // description + og/twitter description
|
|
62
|
+
.keywords(['stats', 'sheets'])
|
|
63
|
+
.favicon('/favicon.png')
|
|
64
|
+
.image('/cover.png') // og:image, twitter:image, twitter:card
|
|
65
|
+
.link('/styles.css') // <link rel=stylesheet> (deduped)
|
|
66
|
+
.style('body{margin:0}') // appends to <style>
|
|
67
|
+
.script({ src: '/app.js' }) // <script> in <head>
|
|
68
|
+
.script({}, 'console.log("hi")', false) // inline <script> in <body>
|
|
69
|
+
.url('/') // canonical + og:url, resolved vs host
|
|
70
|
+
|
|
71
|
+
layout.body.innerHTML = '<h1>Home</h1>'
|
|
72
|
+
console.log(layout.rawHtml) // full HTML string
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## A Layout is an offline DOM
|
|
78
|
+
|
|
79
|
+
This is the core idea: a `Layout` extends an `als-document` document, so the instance is a
|
|
80
|
+
real, in‑memory **DOM tree** you can query and mutate almost exactly like the browser DOM —
|
|
81
|
+
no `jsdom`, no browser, works in plain Node. The chainable `title()/meta()/link()/…` helpers
|
|
82
|
+
are just convenience on top of it; for anything else you manipulate nodes directly.
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
const layout = new Layout(/*html*/`<html><head></head><body><main></main></body></html>`)
|
|
86
|
+
|
|
87
|
+
// Query like the DOM ($ / $$ are aliases for querySelector / querySelectorAll)
|
|
88
|
+
const main = layout.$('main') // === layout.querySelector('main')
|
|
89
|
+
const items = layout.$$('.item') // === querySelectorAll, returns an array
|
|
90
|
+
|
|
91
|
+
// Build and insert nodes
|
|
92
|
+
main.innerHTML = '<h1>Hello</h1>'
|
|
93
|
+
main.insert(2, '<p class="lead">Intro</p>') // append parsed HTML
|
|
94
|
+
main.insertAdjacentHTML('beforeend', '<hr>')
|
|
95
|
+
|
|
96
|
+
// Mutate elements with the familiar API
|
|
97
|
+
const h1 = layout.$('h1')
|
|
98
|
+
h1.setAttribute('id', 'top')
|
|
99
|
+
h1.classList.add('title')
|
|
100
|
+
h1.dataset.role = 'heading'
|
|
101
|
+
h1.textContent = 'Welcome'
|
|
102
|
+
console.log(h1.outerHTML) // <h1 id="top" class="title" data-role="heading">Welcome</h1>
|
|
103
|
+
|
|
104
|
+
// Traverse
|
|
105
|
+
h1.parent // parent node
|
|
106
|
+
h1.nextElementSibling
|
|
107
|
+
layout.body.children
|
|
108
|
+
|
|
109
|
+
// Remove
|
|
110
|
+
layout.$('hr').remove()
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Supported on nodes: `querySelector`/`$`, `querySelectorAll`/`$$`, `getElementById`,
|
|
114
|
+
`getAttribute`/`setAttribute`/`removeAttribute`, `classList`, `dataset`, `style`,
|
|
115
|
+
`innerHTML`/`outerHTML`/`textContent`, `append`/`insert`/`insertAdjacentHTML`/`remove`,
|
|
116
|
+
plus traversal (`parent`, `children`, `prev`/`next`, …). See
|
|
117
|
+
[`als-document`](https://www.npmjs.com/package/als-document) for the full node API.
|
|
118
|
+
|
|
119
|
+
On the client, `toDocument()` flushes this offline DOM into the live `document`.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Cloning
|
|
124
|
+
|
|
125
|
+
`layout.clone` returns a deep, independent copy — ideal as a template for many pages.
|
|
126
|
+
Changes to a clone never affect the original.
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
const base = new Layout(template, 'https://example.com').link('/styles.css')
|
|
130
|
+
const about = base.clone.title('About').url('/about')
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## API
|
|
136
|
+
|
|
137
|
+
### Constructor
|
|
138
|
+
```js
|
|
139
|
+
new Layout(html?: string, host?: string)
|
|
140
|
+
```
|
|
141
|
+
- **html** — optional HTML string to start from.
|
|
142
|
+
- **host** — base URL used to resolve relative URLs in `url()`.
|
|
143
|
+
|
|
144
|
+
### Properties
|
|
145
|
+
- `rawHtml` — current document as an HTML string.
|
|
146
|
+
- `clone` — a deep, independent copy.
|
|
147
|
+
- `urlObj` — the parsed `URL` of the last `url()` call (set after `url()`).
|
|
148
|
+
- `head` / `body` / `html` — element accessors (from `als-document`).
|
|
149
|
+
|
|
150
|
+
### Methods (all chainable, return `this`)
|
|
151
|
+
- `lang(lang)` — sets `<html lang>`.
|
|
152
|
+
- `title(title)` — sets `<title>` and `og:title`.
|
|
153
|
+
- `description(text)` — sets `description`, `og:description`, `twitter:description`.
|
|
154
|
+
- `keywords(list)` — sets/merges `meta[name=keywords]`.
|
|
155
|
+
- `meta(props)` — sets/updates a `<meta>` tag (keyed by its first attribute).
|
|
156
|
+
- `image(src)` — sets `og:image`, `twitter:image`, `twitter:card`.
|
|
157
|
+
- `favicon(href)` — sets/updates the favicon link.
|
|
158
|
+
- `viewport(content?)` — sets the viewport meta.
|
|
159
|
+
- `link(href, attributes?)` — adds a stylesheet `<link>` (skipped if already present).
|
|
160
|
+
- `style(css)` — appends CSS to a `<style>` element.
|
|
161
|
+
- `script(attrs?, innerHTML?, head = true)` — adds a `<script>` (head or body; `src` deduped).
|
|
162
|
+
- `url(url, host = this.URL)` — sets canonical + `og:url`, stores `urlObj`.
|
|
163
|
+
- `toDocument()` — in the browser, renders this layout into the live `document` (no‑op in Node); returns `this`.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Changelog
|
|
168
|
+
|
|
169
|
+
### 7.1.0
|
|
170
|
+
- **Added** `toDocument()` — render a layout into the live DOM on the client.
|
|
171
|
+
- **Added** `urlObj` — `url()` now exposes the parsed `URL`.
|
|
172
|
+
- **Added** `exports` map — ESM resolves to the named `Layout` export, CommonJS to the class.
|
|
173
|
+
- **Fixed** `twitter:description` now uses `name=` (was `property=`), per the Twitter Cards spec.
|
|
174
|
+
- **Fixed** `clone` preserves the document URL/host.
|
|
175
|
+
- **Changed** `url()` keeps the canonical trailing slash (`URL.href`), and stores `urlObj`.
|
|
176
|
+
|
|
177
|
+
### 7.0.0 (breaking)
|
|
178
|
+
- Merged into a single source file; added ESM build (`index.mjs`) and browser bundle (`layout.js`).
|
|
179
|
+
- Removed `status`, `end`, `minified`, `propsToClone`, and JS/CSS uglification.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Notes
|
|
184
|
+
- Pure document builder — no global state, no file I/O.
|
|
185
|
+
- `index.mjs` and the browser `layout.js` are generated from `src/layout.js` by `node build.js`.
|
|
186
|
+
- Use `clone` to derive page variants from a base template.
|
package/src/layout.js
CHANGED
|
@@ -1,85 +1,93 @@
|
|
|
1
|
-
const { Document, SingleNode, Node } = require('als-document');
|
|
2
|
-
class Layout extends Document {
|
|
3
|
-
constructor(html, host) { super(html, host); this.root = this.html; }
|
|
4
|
-
get rawHtml() { return this.innerHTML }
|
|
5
|
-
get clone() { return new this.constructor(new Document(this, this.URL), this.
|
|
6
|
-
lang(lang) { this.html.setAttribute('lang', lang); return this }
|
|
7
|
-
link(href, attributes = { rel: "stylesheet", type: "text/css" }) {
|
|
8
|
-
if (!href || typeof href !== 'string') throw new Error(`href attribute must be a string`)
|
|
9
|
-
if (!this.root.querySelector(`link[rel=stylesheet][href^="${href}"]`))
|
|
10
|
-
this.head.insert(2, new SingleNode('link', { href, ...attributes }))
|
|
11
|
-
return this
|
|
12
|
-
}
|
|
13
|
-
keywords(keywords = []) {
|
|
14
|
-
let el = this.root.$('meta[name=keywords]') || new SingleNode('meta', { name: 'keywords' })
|
|
15
|
-
keywords = new Set([...(el.getAttribute('content') || '').split(','),...keywords.map(k => k.trim())].filter(Boolean))
|
|
16
|
-
if (keywords.size) el.setAttribute('content', Array.from(keywords).join(','))
|
|
17
|
-
if(keywords.size && !el.parent) this.head.insert(2, el)
|
|
18
|
-
return this
|
|
19
|
-
}
|
|
20
|
-
style(styles) {
|
|
21
|
-
if (typeof styles !== 'string') throw 'styles parameter should be string';
|
|
22
|
-
let el = this.root.$('style') || this.head.insert(2, new Node('style'))
|
|
23
|
-
el.innerHTML = el.innerHTML + '\n' + styles;
|
|
24
|
-
return this
|
|
25
|
-
}
|
|
26
|
-
url(url, host = this.URL) {
|
|
27
|
-
try {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
el.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (attrs
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
this.meta({
|
|
56
|
-
this.meta({ property: '
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
this.meta({
|
|
74
|
-
this.meta({ name: 'twitter:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
super.title
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
1
|
+
const { Document, SingleNode, Node } = require('als-document');
|
|
2
|
+
class Layout extends Document {
|
|
3
|
+
constructor(html, host) { super(html, host); this.root = this.html; }
|
|
4
|
+
get rawHtml() { return this.innerHTML }
|
|
5
|
+
get clone() { return new this.constructor(new Document(this, this.URL), this.URL) }
|
|
6
|
+
lang(lang) { this.html.setAttribute('lang', lang); return this }
|
|
7
|
+
link(href, attributes = { rel: "stylesheet", type: "text/css" }) {
|
|
8
|
+
if (!href || typeof href !== 'string') throw new Error(`href attribute must be a string`)
|
|
9
|
+
if (!this.root.querySelector(`link[rel=stylesheet][href^="${href}"]`))
|
|
10
|
+
this.head.insert(2, new SingleNode('link', { href, ...attributes }))
|
|
11
|
+
return this
|
|
12
|
+
}
|
|
13
|
+
keywords(keywords = []) {
|
|
14
|
+
let el = this.root.$('meta[name=keywords]') || new SingleNode('meta', { name: 'keywords' })
|
|
15
|
+
keywords = new Set([...(el.getAttribute('content') || '').split(','),...keywords.map(k => k.trim())].filter(Boolean))
|
|
16
|
+
if (keywords.size) el.setAttribute('content', Array.from(keywords).join(','))
|
|
17
|
+
if(keywords.size && !el.parent) this.head.insert(2, el)
|
|
18
|
+
return this
|
|
19
|
+
}
|
|
20
|
+
style(styles) {
|
|
21
|
+
if (typeof styles !== 'string') throw 'styles parameter should be string';
|
|
22
|
+
let el = this.root.$('style') || this.head.insert(2, new Node('style'))
|
|
23
|
+
el.innerHTML = el.innerHTML + '\n' + styles;
|
|
24
|
+
return this
|
|
25
|
+
}
|
|
26
|
+
url(url, host = this.URL) {
|
|
27
|
+
try {
|
|
28
|
+
this.urlObj = (host ? new URL(url, host) : new URL(url))
|
|
29
|
+
url = this.urlObj.href
|
|
30
|
+
this.meta({ property: 'og:url', content: url })
|
|
31
|
+
const el = this.root.$('link[rel="canonical"]') || this.head.insert(2, new SingleNode('link', { rel: 'canonical', href: url }))
|
|
32
|
+
el.setAttribute('href', url)
|
|
33
|
+
} catch (error) { error.info = `url "${url}" with host "${host}" is not valid url`; throw error; }
|
|
34
|
+
return this
|
|
35
|
+
}
|
|
36
|
+
meta(props) {
|
|
37
|
+
const entries = Object.entries(props)
|
|
38
|
+
const [name, value] = entries[0]
|
|
39
|
+
const metaElement = this.root.querySelector(`meta[${name}="${value}"]`) || this.head.insert(2, new SingleNode('meta', props))
|
|
40
|
+
entries.forEach(([name, v]) => metaElement.setAttribute(name, props[name]))
|
|
41
|
+
return this
|
|
42
|
+
}
|
|
43
|
+
script(attrs = {}, innerHTML = '', head = true) {
|
|
44
|
+
if (typeof attrs !== 'object' || attrs === null || Array.isArray(attrs)) attrs = {}
|
|
45
|
+
if (attrs.src && this.root.querySelector(`script[src="${attrs.src}"]`)) return this
|
|
46
|
+
if (Object.keys(attrs).length || innerHTML) {
|
|
47
|
+
const script = new Node('script', attrs)
|
|
48
|
+
if (innerHTML) script.innerHTML = innerHTML
|
|
49
|
+
if (head) this.head.insert(2, script)
|
|
50
|
+
else this.html.insert(2, script)
|
|
51
|
+
}
|
|
52
|
+
return this
|
|
53
|
+
}
|
|
54
|
+
description(description) {
|
|
55
|
+
this.meta({ name: 'description', content: description })
|
|
56
|
+
this.meta({ property: 'og:description', content: description })
|
|
57
|
+
this.meta({ name: 'twitter:description', content: description })
|
|
58
|
+
return this
|
|
59
|
+
}
|
|
60
|
+
favicon(href) {
|
|
61
|
+
const el = this.root.$('link[rel=icon][type=image/x-icon]')
|
|
62
|
+
if (el) el.setAttribute('href', href)
|
|
63
|
+
else this.head.insert(2, new SingleNode('link', { rel: 'icon', href, type: 'image/x-icon' }))
|
|
64
|
+
return this
|
|
65
|
+
}
|
|
66
|
+
viewport(viewport = 'width=device-width, initial-scale=1.0') {
|
|
67
|
+
const el = this.root.$('meta[name="viewport"]')
|
|
68
|
+
if (el) el.setAttribute('content', viewport)
|
|
69
|
+
else this.head.insert(2, new SingleNode('meta', { name: 'viewport', content: viewport }))
|
|
70
|
+
return this
|
|
71
|
+
}
|
|
72
|
+
image(image) {
|
|
73
|
+
this.meta({ property: 'og:image', content: image })
|
|
74
|
+
this.meta({ name: 'twitter:image', content: image })
|
|
75
|
+
this.meta({ name: 'twitter:card', content: 'summary_large_image' })
|
|
76
|
+
return this
|
|
77
|
+
}
|
|
78
|
+
title(title) {
|
|
79
|
+
super.title // create title tag if not exists
|
|
80
|
+
super.title = title
|
|
81
|
+
this.meta({ property: 'og:title', content: title })
|
|
82
|
+
return this
|
|
83
|
+
}
|
|
84
|
+
toDocument() {
|
|
85
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') return this
|
|
86
|
+
document.querySelector('title')?.remove()
|
|
87
|
+
document.head.innerHTML = this.head.innerHTML
|
|
88
|
+
document.body.outerHTML = this.body.outerHTML
|
|
89
|
+
return this
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = Layout
|
package/build.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
const { writeFileSync, readFileSync } = require('fs')
|
|
2
|
-
const Layout = require('./src/layout.js');
|
|
3
|
-
const content = Layout.toString()
|
|
4
|
-
|
|
5
|
-
writeFileSync('index.mjs',`import { Document, SingleNode, Node } from 'als-document';\n`+'export '+content,'utf8' )
|
|
6
|
-
const document = readFileSync('./node_modules/als-document/document.js','utf8')
|
|
7
|
-
|
|
8
|
-
writeFileSync('layout.js',[document,`const { Document, SingleNode, Node } = alsDocument`,content].join('\n'))
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
const assert = require('assert');
|
|
2
|
-
const { describe, it } = require('node:test')
|
|
3
|
-
const Layout = require('../src/layout.js');
|
|
4
|
-
|
|
5
|
-
describe('Layout Initialization', () => {
|
|
6
|
-
it('should create an instance of Layout', () => {
|
|
7
|
-
const layout = new Layout();
|
|
8
|
-
assert(layout instanceof Layout, 'layout is not an instance of Layout');
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it('should initialize default language as "en"', () => {
|
|
12
|
-
const layout = new Layout();
|
|
13
|
-
assert(layout.html.getAttribute('lang') === 'en', 'default language is not "en"');
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('should allow setting a custom language', () => {
|
|
17
|
-
const customLang = 'fr';
|
|
18
|
-
const layout = new Layout(undefined).lang(customLang);
|
|
19
|
-
layout.lang(customLang)
|
|
20
|
-
assert(layout.html.getAttribute('lang') === customLang, `language is not set to ${customLang}`);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('should allow setting a custom host', () => {
|
|
24
|
-
const customHost = 'http://localhost';
|
|
25
|
-
const layout = new Layout(undefined,customHost);
|
|
26
|
-
assert.strictEqual(layout.URL, customHost, `host is not set to ${customHost}`);
|
|
27
|
-
});
|
|
28
|
-
});
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
const assert = require('assert');
|
|
2
|
-
const { describe, it,beforeEach } = require('node:test')
|
|
3
|
-
const Layout = require('../src/layout.js');
|
|
4
|
-
|
|
5
|
-
describe('description and title', () => {
|
|
6
|
-
let layout;
|
|
7
|
-
|
|
8
|
-
beforeEach(() => layout = new Layout());
|
|
9
|
-
|
|
10
|
-
it('should add description correctly', () => {
|
|
11
|
-
const description = 'Test Description';
|
|
12
|
-
layout.description(description);
|
|
13
|
-
assert.strictEqual(layout.root.$('meta[name="description"]').getAttribute('content'), description, 'Description not set correctly');
|
|
14
|
-
assert.strictEqual(layout.root.$('meta[property="og:description"]').getAttribute('content'), description, 'Description not set correctly');
|
|
15
|
-
assert.strictEqual(layout.root.$('meta[property="twitter:description"]').getAttribute('content'), description, 'Description not set correctly');
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('should add title correctly', () => {
|
|
19
|
-
const title = 'Test Title';
|
|
20
|
-
layout.title(title);
|
|
21
|
-
assert(layout.root.$('title').innerHTML === title, 'Title not set correctly');
|
|
22
|
-
assert(layout.root.$('[property="og:title"]').getAttribute('content') === title, 'Title not set correctly');
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('should add title if no title tag', () => {
|
|
26
|
-
layout.$('title').remove()
|
|
27
|
-
const title = 'Test Title';
|
|
28
|
-
layout.title(title);
|
|
29
|
-
assert(layout.root.$('title').innerHTML === title, 'Title not set correctly');
|
|
30
|
-
assert(layout.root.$('[property="og:title"]').getAttribute('content') === title, 'Title not set correctly');
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
});
|