├── .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 | --------------------------------------------------------------------------------