├── .gitignore
├── test
├── mocha.opts
├── fixtures
│ ├── single_line.html
│ ├── undefined.html
│ ├── simple.html
│ ├── boolean.html
│ ├── preceding_whitespace.html
│ ├── dirty.html
│ ├── colons_in_values.html
│ ├── not_at_the_top.html
│ ├── wrapped.html
│ ├── numeric.html
│ ├── multi_comment.html
│ ├── arrays.html
│ ├── dates.html
│ └── cordova.html
└── index.js
├── .travis.yml
├── package.json
├── index.js
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --reporter spec
2 | --ui tdd
3 |
--------------------------------------------------------------------------------
/test/fixtures/single_line.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Hello World
4 |
--------------------------------------------------------------------------------
/test/fixtures/undefined.html:
--------------------------------------------------------------------------------
1 |
4 | Hello World
5 |
--------------------------------------------------------------------------------
/test/fixtures/simple.html:
--------------------------------------------------------------------------------
1 |
4 |
5 | Hello World
6 |
--------------------------------------------------------------------------------
/test/fixtures/boolean.html:
--------------------------------------------------------------------------------
1 |
5 |
6 | Hello World
7 |
--------------------------------------------------------------------------------
/test/fixtures/preceding_whitespace.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 | Hello World
9 |
--------------------------------------------------------------------------------
/test/fixtures/dirty.html:
--------------------------------------------------------------------------------
1 |
6 |
7 | Hello World
8 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: node_js
3 | node_js:
4 | - "0.12"
5 | - "0.10"
6 | - iojs
7 | notifications:
8 | email: false
9 |
--------------------------------------------------------------------------------
/test/fixtures/colons_in_values.html:
--------------------------------------------------------------------------------
1 |
5 |
6 | Hello World
7 |
--------------------------------------------------------------------------------
/test/fixtures/not_at_the_top.html:
--------------------------------------------------------------------------------
1 | Hello World
2 |
3 |
7 |
--------------------------------------------------------------------------------
/test/fixtures/wrapped.html:
--------------------------------------------------------------------------------
1 |
7 |
8 | Hello World
9 |
--------------------------------------------------------------------------------
/test/fixtures/numeric.html:
--------------------------------------------------------------------------------
1 |
7 |
8 | Hello World
9 |
--------------------------------------------------------------------------------
/test/fixtures/multi_comment.html:
--------------------------------------------------------------------------------
1 |
4 |
5 | Watch out for other comments
6 |
7 |
10 |
--------------------------------------------------------------------------------
/test/fixtures/arrays.html:
--------------------------------------------------------------------------------
1 |
10 |
11 | Hello World
12 |
--------------------------------------------------------------------------------
/test/fixtures/dates.html:
--------------------------------------------------------------------------------
1 |
9 |
10 | Date Parsing is Fun™
11 |
--------------------------------------------------------------------------------
/test/fixtures/cordova.html:
--------------------------------------------------------------------------------
1 |
21 |
22 | # cordova-cli
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "html-frontmatter",
3 | "version": "1.6.1",
4 | "description": "Extract key-value metadata from HTML comments",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "mocha && standard"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/zeke/html-frontmatter"
12 | },
13 | "keywords": [
14 | "html",
15 | "frontmatter",
16 | "comments",
17 | "metadata"
18 | ],
19 | "author": "Zeke Sikelianos (http://zeke.sikelianos.com/)",
20 | "license": "MIT",
21 | "bugs": {
22 | "url": "https://github.com/zeke/html-frontmatter/issues"
23 | },
24 | "homepage": "https://github.com/zeke/html-frontmatter",
25 | "devDependencies": {
26 | "mocha": "^3.2.0",
27 | "standard": "^7.1.0"
28 | },
29 | "dependencies": {
30 | "dateutil": "^0.1.0"
31 | },
32 | "standard": {
33 | "env": {
34 | "mocha": true
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const dateutil = require('dateutil')
4 | const pattern = new RegExp('^(?:\r\n?|\n)*') // eslint-disable-line
5 |
6 | const parse = module.exports = function parse (input) {
7 | if (!input.match(pattern)) return
8 |
9 | var obj = {}
10 |
11 | pattern
12 | .exec(input)[1]
13 | .replace(/(\r\n?|\n){2,}/g, '\n') // remove excess newlines
14 | .replace(/(\r\n?|\n) {2,}/g, ' ') // treat two-space indentation as a wrapped line
15 | // .replace(/[ \t]{2,}/g, ' ') // remove excess spaces or tabs (but no new lines)
16 | .split('\n')
17 | .forEach(function (line) {
18 | if (line.match(/^\s?#/)) return // ignore lines starting with #
19 | var parts = line.split(/:(.+)?/) // split on _first_ colon
20 | if (parts.length < 2) return // key: value pair is required
21 | var key = (parts[0] || '').trim()
22 | var value = (parts[1] || '').trim()
23 |
24 | value = coerceValue(value)
25 |
26 | if (value[0] === '[' && value[value.length - 1] === ']') {
27 | value = value.substring(1, value.length - 1).trim().split(/\s*,\s*/).map(function (val) {
28 | return coerceValue(val)
29 | })
30 | }
31 |
32 | obj[key] = value
33 | })
34 |
35 | function coerceValue (value) {
36 | // boolean
37 | if (value === 'true') return true
38 | if (value === 'false') return false
39 |
40 | // date (within 200 years of today's date)
41 | var date = dateutil.parse(value)
42 | if (date.type !== 'unknown_date') {
43 | delete date.type
44 | delete date.size
45 | var now = new Date().getFullYear()
46 | var then = date.getFullYear()
47 | if (Math.abs(now - then) < 200) return date
48 | }
49 |
50 | // number
51 | var num = +value
52 | if (num) return num
53 |
54 | return value
55 | }
56 |
57 | return obj
58 | }
59 |
60 | parse.pattern = pattern
61 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert')
2 | const fs = require('fs')
3 | const path = require('path')
4 | const fm = require('..')
5 |
6 | var fixtures = {}
7 | fs.readdirSync(path.join(__dirname, 'fixtures')).forEach(function (file) {
8 | var key = path.basename(file).replace('.html', '')
9 | fixtures[key] = fs.readFileSync(path.join(__dirname, 'fixtures', file)).toString()
10 | })
11 |
12 | describe('html-frontmatter', function () {
13 | // Essence
14 |
15 | it('extracts metadata from colon-delimited comments at the top of an HTML string', function () {
16 | assert.deepEqual(fm(fixtures.simple), {foo: 'bar'})
17 | })
18 |
19 | it('returns null if frontmatter is not found', function () {
20 | assert.equal(fm('blah'), null)
21 | })
22 |
23 | it('handles values that contain colons', function () {
24 | assert.equal(fm(fixtures.colons_in_values).title, 'How I roll: or, the life of a wheel')
25 | })
26 |
27 | it('handles line-wrapped values', function () {
28 | assert.equal(fm(fixtures.wrapped).description, 'This is a line that wraps and wraps badly')
29 | })
30 |
31 | it('cleans up excess whitespace', function () {
32 | assert.deepEqual(fm(fixtures.dirty), {badly: 'spaced', crappy: 'input'})
33 | })
34 |
35 | it('ignores comments that are not at the top of the file', function () {
36 | assert.equal(fm(fixtures.not_at_the_top), null)
37 | })
38 |
39 | it('allows newlines before comments', function () {
40 | assert.deepEqual(fm(fixtures.preceding_whitespace), {foo: 'bar'})
41 | })
42 |
43 | it('ignores comment lines starting with hashes (#)', function () {
44 | assert(fm(fixtures.cordova))
45 | })
46 |
47 | it('allows single-line comments', function () {
48 | assert.deepEqual(fm(fixtures.single_line), {foo: 'bar'})
49 | })
50 |
51 | it('does not include additional comments', function () {
52 | assert.deepEqual(fm(fixtures.multi_comment), { foo: 'bar' })
53 | })
54 |
55 | // Coercion
56 |
57 | it('coerces boolean strings into Booleans', function () {
58 | assert.deepEqual(fm(fixtures.boolean), {good: true, bad: false})
59 | })
60 |
61 | it('coerces numeric strings into Numbers', function () {
62 | var n = fm(fixtures.numeric)
63 | assert.strictEqual(n.integral, 10000000)
64 | assert.strictEqual(n.decimal, 3.1415)
65 | assert.strictEqual(n.negative, -100)
66 | assert.strictEqual(n.stringy, 'I am not a number')
67 | })
68 |
69 | it('coerces YMD-ish date strings into Dates', function () {
70 | var n = fm(fixtures.dates)
71 | assert.strictEqual(n.date1.getFullYear(), 2012)
72 | assert.strictEqual(n.date1.getMonth(), 4)
73 | assert.strictEqual(n.date1.getDate(), 31)
74 | assert.strictEqual(n.date2.getFullYear(), 2012)
75 | assert.strictEqual(n.date3.getFullYear(), 2012)
76 | assert.strictEqual(n.not_a_date1, 200)
77 | assert.strictEqual(n.not_a_date2, 'cheese')
78 | assert.strictEqual(n.not_a_date3, '2015 people')
79 | })
80 |
81 | // Convenience
82 |
83 | it('exposes its regex pattern as `pattern`', function () {
84 | assert(fm.pattern)
85 | })
86 |
87 | it('handles missing right-hand-value', function () {
88 | assert.deepEqual(fm(fixtures.undefined), {demobox: ''})
89 | })
90 |
91 | // Arrays
92 |
93 | it('handles shallow arrays', function () {
94 | var n = fm(fixtures.arrays)
95 | assert.deepEqual(n.flow, ['one', 'two'])
96 | assert.deepEqual(n.spanning, ['one', 'two', 'three'])
97 | assert.deepEqual(n.coercion, [ 1, 'I am not a number', false, true ])
98 | })
99 | })
100 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # HTML Frontmatter
2 |
3 | Extract key-value metadata from HTML comments
4 |
5 | In the world of printed books, [front
6 | matter](http://en.wikipedia.org/wiki/Book_design#Front_matter) is the stuff
7 | at the beginning of the book like the title page, foreword, preface, table
8 | of contents, etc. In the world of computer programming, frontmatter is metadata at the top
9 | of a file. The term was (probably) popularized by the [Jekyll static site
10 | generator](http://jekyllrb.com/docs/frontmatter/).
11 |
12 | Unlike YAML frontmatter though, HTML frontmatter lives inside plain old HTML comments, so it will be
13 | quietly ignored by tools/browsers that don't know about it.
14 |
15 | ## Installation
16 |
17 | Download node at [nodejs.org](http://nodejs.org) and install it, if you haven't already.
18 |
19 | ```sh
20 | npm install html-frontmatter --save
21 | ```
22 |
23 | ## Usage
24 |
25 | Given an HTML or Markdown file that looks like this:
26 |
27 | ```html
28 |
34 |
35 | Hello World
36 | ```
37 |
38 | And code like this:
39 |
40 | ```js
41 | var fm = require('html-frontmatter')
42 | var frontmatter = fm(fs.readFileSync('github.md', 'utf-8'))
43 | ```
44 |
45 | Here's what you'll get:
46 |
47 | ```js
48 | {
49 | title: "GitHub Integration",
50 | keywords: "github, git, npm, enterprise",
51 | published: "Wed Oct 01 2014 17:00:00 GMT-0700 (PDT)",
52 | description: "npmE works with GitHub!"
53 | }
54 | ```
55 |
56 | ### Multiline Values
57 |
58 | If you have a long string (like a description) and want it to span multiple
59 | lines, simply indent each subsequent line with *2 or more* spaces:
60 |
61 | ```html
62 |
67 | ```
68 |
69 | ### Colons in Values
70 |
71 | Your values can contain colons. No worries.
72 |
73 | ```html
74 |
77 | ```
78 |
79 | ### Array Values
80 |
81 | Your values can include shallow arrays
82 |
83 | ```html
84 |
88 | ```
89 |
90 | Is equivalent to:
91 |
92 | ```html
93 |
102 | ```
103 |
104 | And will return:
105 |
106 | ```js
107 | {
108 | title: "This post has tags",
109 | tags: [100, 'this is a string', true]
110 | }
111 | ```
112 |
113 | ### Coercion
114 |
115 | - Boolean "true" and "false" strings are converted to Boolean.
116 | - Numeric strings are converted to Number.
117 | - Strings in [YMD-ish format](https://github.com/borgar/dateutil#dateutilparse-string-)
118 | are converted to Date objects.
119 |
120 | ### Under the Hood
121 |
122 | html-frontmatter exposes the regular expression it uses to detect presence
123 | of frontmatter as a property named `pattern`. You can use it to
124 | conditionally parse frontmatter:
125 |
126 | ```js
127 | var fm = require('html-frontmatter')
128 | var content = "A string that doesn't have frontmatter in it"
129 | if (content.match(fm.pattern)) {
130 | // nope
131 | }
132 | ```
133 |
134 |
135 | ## Tests
136 |
137 | ```sh
138 | npm install
139 | npm test
140 |
141 | # ✓ extracts metadata from colon-delimited comments at the top of an HTML string
142 | # ✓ returns null if frontmatter is not found
143 | # ✓ handles values that contain colons
144 | # ✓ handles line-wrapped values
145 | # ✓ cleans up excess whitespace
146 | # ✓ ignores comments that are not at the top of the file
147 | # ✓ allows newlines before comments
148 | # ✓ ignores comment lines starting with hashes (#)
149 | # ✓ allows single-line comments
150 | # ✓ does not include additional comments
151 | # ✓ coerces boolean strings into Booleans
152 | # ✓ coerces numeric strings into Numbers
153 | # ✓ coerces YMD-ish date strings into Dates
154 | # ✓ exposes its regex pattern as `pattern`
155 | # ✓ handles missing right-hand-value
156 | # ✓ handles shallow arrays
157 | ```
158 |
--------------------------------------------------------------------------------