Skip to content

Instantly share code, notes, and snippets.

@tdolsen
Last active December 13, 2016 19:05
Show Gist options
  • Save tdolsen/0bc847185965d11294eed2405ed43175 to your computer and use it in GitHub Desktop.
Save tdolsen/0bc847185965d11294eed2405ed43175 to your computer and use it in GitHub Desktop.
A mini CMS - yaml/markdown hybrid flat file mess.
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));
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;
});
}
}

title: Bla bla bla

Markdown

Some article content goes here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment