Some article content goes here.
Last active
December 13, 2016 19:05
-
-
Save tdolsen/0bc847185965d11294eed2405ed43175 to your computer and use it in GitHub Desktop.
A mini CMS - yaml/markdown hybrid flat file mess.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as path from "path"; | |
import * as express from "express"; | |
import { Collection } from "./collection"; | |
let collection = new Collection(path.join(__dirname, "data")); // Imagine the `data-(.*)´ files in this gist are actually `data/$1` | |
let app = express(); | |
app.get("/latest", (req, res) => collection.getLatest(10).then(res.json)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import fs = require("fs-extra"); | |
import glob = require("glob"); | |
import readline = require("readline"); | |
import path = require("path"); | |
export type Header = { key: string, value: string }; | |
export type Order = { [header: string]: "ASC"|"DESC" }; | |
export interface File { | |
file: string; | |
headers: { | |
slug: string; | |
date: Date; | |
[key: string]: any; | |
}; | |
body: any; | |
}; | |
export class Collection { | |
public get collection_path() : string { | |
return this._collection_path; | |
} | |
public set collection_path(path: string) { | |
try { | |
// Ensure path is a directory, or try creating it. | |
fs.ensureDirSync(path); | |
// Set path. | |
this._collection_path = path; | |
} catch (e) { | |
console.log(e); | |
} | |
} | |
private _collection: any[]; | |
private _collection_path: string; | |
constructor(collection_path: string) { | |
this.collection_path = collection_path; | |
} | |
public getLatest(n: number = 0, order?: Order) : Promise<File[]> { | |
return this._glob() | |
.then(f => this._files(f)) | |
.then(f => this._order(f, order)) | |
// .then(d => {console.log("order", this.name, d[0]["headers"]); return d}) | |
// .then(f => this._limit(f, x)) | |
; | |
} | |
public getSlug(slug: string) : Promise<File> { | |
return this._glob(undefined, undefined, undefined, slug) | |
.then(f => this._files(f)) | |
.then(f => this._order(f, { date: "DESC" })) | |
.then(f => f[0]) | |
; | |
} | |
// public getYear(year: number) { | |
// let date = `${year}-[0-1][0-9]-[0-3][0-9]`; | |
// } | |
// | |
// public getMonth(year: number, month: number) { | |
// | |
// } | |
/** | |
* Helper to make glob calls matching file names easily. | |
* @private | |
* @param {number|string} year | |
* @param {number|string} month | |
* @param {number|string} day | |
* @param {string} slug | |
* @param {string} ext | |
* @return {Promise<string[]>} Returns a list of string files names. | |
*/ | |
private _glob = ( | |
year: number|string = "[0-9][0-9][0-9][0-9]", | |
month: number|string = "[0-1][0-9]", | |
day: number|string = "[0-3][0-9]", | |
slug: string = "*", | |
ext: string = "md" | |
) : Promise<string[]> => { | |
return Promise.resolve(glob.sync(`${year}-${month}-${day}[ -_]${slug}.${ext}`, { cwd: this.collection_path })); | |
} | |
/** | |
* Promise chain method to map a list of files names for processing. | |
* @private | |
* @param {string[]} list | |
* @return {Promise<File[]>} Returns a promise of the processed files. | |
*/ | |
private _files = (list: string[]) : Promise<File[]> => { | |
return Promise.all(list.map(f => this._file(f))); | |
} | |
/** | |
* Method to process a single file given a file name. | |
* @private | |
* @param {string} file | |
* @return {Promise<File>} Returns a promise of a processed File. | |
*/ | |
private _file = (file: string) : Promise<File> => { | |
return new Promise((resolve, reject) => { | |
// Match name for date and slug. | |
let name_matches = file.match(/^(([0-9]{4})-([0-9]{2})-([0-9]{2}))[ -_](.*)\.([^\.]*)/); | |
if (!name_matches) { throw new Error("file name does not match expected format"); } | |
// Set variables for encolsure. | |
let slug = name_matches[5]; | |
let date = new Date(name_matches[1]); | |
// Variables to control line reading. | |
let is_header = true; | |
let lines: { | |
headers: Header[], | |
body: string[] | |
} = { | |
headers: [], | |
body: [] | |
}; | |
// Set up line reader. | |
let reader = readline.createInterface({ | |
input: fs.createReadStream(path.join(this.collection_path, file)) | |
}); | |
// Attach to "line" event. | |
reader.on("line", (line) => { | |
// Set end of headers on first line starting with "---". | |
if (is_header && line.match(/^---/)) { | |
is_header = false; | |
return; | |
} | |
// Push line to either headers or body. | |
lines[is_header ? "headers" : "body"].push(is_header ? this._header(line) : line); | |
}); | |
// Attach to "close" event. | |
reader.on("close", () => { | |
// Process headers, setting key value. | |
let headers = { | |
slug: slug, | |
date: date, | |
}; | |
for (let header of lines.headers) { | |
headers[header.key] = header.value; | |
} | |
// Process body, removing leading blank lines, joining as string. | |
while (lines.body.length > 0 && lines.body[0] === "") { | |
lines.body.shift(); | |
} | |
let body = lines.body.join("\n"); | |
// Resolve with all gathered data. | |
resolve({ | |
file: file, | |
headers: headers, | |
body: body | |
}); | |
}); | |
}); | |
} | |
/** | |
* Processes a single header line. | |
* @private | |
* @param {string} line | |
* @return {null|Header} Returns a Header or null if no match was found. | |
*/ | |
private _header = (line: string) : null | Header => { | |
let matches = line.match(/^(.*):\s(.*)+/); | |
if (!matches) return null; | |
return { | |
key: matches[1], | |
value: matches[2] | |
}; | |
} | |
private _order = (files: File[], order: Order = { date: "DESC" }) : File[] => { | |
return files.sort((a, b) => { | |
// Loop over orders. | |
for (let key in order) { | |
// Continue loop if equal. | |
if (a.headers[key] === b.headers[key]) continue; | |
// Store return value without direction adjustment. | |
let ret = a.headers[key] > b.headers[key] ? 1 : -1; | |
// Return value with direction. | |
return order[key] === "ASC" ? ret : ret * -1; | |
} | |
// Return equal, since no difference was found. | |
return 0; | |
}); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment