gemfedwiki

gemini fedwiki proxy
git clone git://jb55.com/gemfedwiki
Log | Files | Refs | README

index.js (4818B)


      1 const request = require('superagent')
      2 const fs = require('fs')
      3 const {createServer} = require('@derhuerst/gemini')
      4 
      5 function slugify(text)
      6 {
      7 	return text.replace(/\s/g, '-').replace(/[^A-Za-z0-9-]/g, '').toLowerCase()
      8 }
      9 
     10 function consume_until(cursor, c)
     11 {
     12 	for (; cursor.pos < cursor.str.length; cursor.pos++) {
     13 		if (cursor.str[cursor.pos] == c[0])
     14 			return true
     15 	}
     16 	return false
     17 }
     18 
     19 function pull_char(cursor)
     20 {
     21 	if (cursor.pos >= cursor.str.length)
     22 		return null
     23 	return cursor.str[cursor.pos++]
     24 }
     25 
     26 function consume_char(cursor, match)
     27 {
     28 	return pull_char(cursor) === match[0]
     29 }
     30 
     31 function parse_slug_link(cursor, links, root, start)
     32 {
     33 	let i = cursor.pos
     34 	if (!consume_until(cursor, ']')) return false
     35 	const name = cursor.str.slice(i, cursor.pos)
     36 	const url = `${root}/${slugify(name)}`
     37 	if (!consume_char(cursor, ']')) return false
     38 	if (!consume_char(cursor, ']')) return false
     39 	const orig = cursor.str.slice(start, cursor.pos)
     40 	links.push({name, url, orig})
     41 	return true
     42 }
     43 
     44 function consume_whitespace(cursor)
     45 {
     46 	for (; cursor.pos < cursor.str.length; cursor.pos++) {
     47 		if (cursor.str[cursor.pos] !== ' ')
     48 			return;
     49 	}
     50 }
     51 
     52 function parse_external_link(cursor, links, start)
     53 {
     54 	consume_whitespace(cursor)
     55 	let i = cursor.pos
     56 
     57 	for (; cursor.pos < cursor.str.length; cursor.pos++) {
     58 		if (cursor.str[cursor.pos] === ']')
     59 			return false;
     60 		if (cursor.str[cursor.pos] === ' ')
     61 			break
     62 	}
     63 
     64 	url = cursor.str.slice(i, cursor.pos++)
     65 	i = cursor.pos
     66 
     67 	if (!consume_until(cursor, "]")) return false
     68 	name = cursor.str.slice(i, cursor.pos++).trim()
     69 
     70 	const orig = cursor.str.slice(start, cursor.pos)
     71 
     72 	links.push({ name, url, orig })
     73 	return true
     74 }
     75 
     76 function parse_link(cursor, links, root)
     77 {
     78 	let c, i, url, name
     79 
     80 	let start = cursor.pos
     81 
     82 	if (!consume_char(cursor, "["))
     83 		return false
     84 
     85 	if ((c = pull_char(cursor)) && c == '[')
     86 		return parse_slug_link(cursor, links, root, start)
     87 
     88 	cursor.pos--
     89 	return parse_external_link(cursor, links, start)
     90 }
     91 
     92 function parse_links(text, root)
     93 {
     94 	let cursor = {pos: 0, str: text}
     95 	let links = []
     96 
     97 	for (; cursor.pos < cursor.str.length;) {
     98 		if (!consume_until(cursor, "["))
     99 			return links
    100 
    101 		if (!parse_link(cursor, links, root))
    102 			continue
    103 	}
    104 
    105 	return links
    106 }
    107 
    108 function process_item(root, lines, item)
    109 {
    110 	let first = true;
    111 	let i = 0
    112 
    113 	if (item.type === 'paragraph' || item.type === 'markdown' || item.type === 'html') {
    114 		const text = item.text.replace(/\n/g, " ").trim()
    115 		const links = parse_links(text, root)
    116 
    117 		lines.push('')
    118 		lines.push(text)
    119 		i = lines.length-1
    120 
    121 		if (links.length !== 0)
    122 			lines.push('')
    123 
    124 		for (const {url, name, orig} of links) {
    125 			lines[i] = lines[i].replace(orig, name)
    126 			if (lines[i] === name)
    127 				// just remove it if it's the same as the link
    128 				lines.splice(i, 2)
    129 			lines.push(`=> ${url} ${name}`)
    130 		}
    131 
    132 	}
    133 }
    134 
    135 function get_forks(journal)
    136 {
    137 	let forks = []
    138 	for (const item of journal)
    139 	{
    140 		if (item.type === "fork" && item.site)
    141 			forks.push(item.site)
    142 	}
    143 
    144 	return forks
    145 }
    146 
    147 function fedwiki2gmi(root, article)
    148 {
    149 	let lines = []
    150 	lines.push(`# ${article.title}`)
    151 
    152 	//const forks = get_fork(article.journal)
    153 	//root = fork || root
    154 
    155 	for (const item of article.story) {
    156 		process_item(root, lines, item)
    157 	}
    158 
    159 	return lines.join("\n") + "\n"
    160 }
    161 
    162 function last(arr)
    163 {
    164 	if (arr.length === 0)
    165 		return null
    166 	return arr[arr.length - 1]
    167 }
    168 
    169 async function make_request(json_url, root="")
    170 {
    171 	const res = await request.get(json_url)
    172 	return fedwiki2gmi(root, res.body)
    173 }
    174 
    175 async function make_index_request(host, root="")
    176 {
    177 	const res = await request.get(host + '/')
    178 	const file = last(res.redirects)
    179 	const json = file.replace(/.html$/, ".json")
    180 	return await make_request(json, root)
    181 }
    182 
    183 async function handle_request(req, res)
    184 {
    185 	console.log(req.path)
    186 
    187 	if (!req.path || req.path === "" || req.path === "/") {
    188 		fs.readFile("index.gmi", (err, dat) => {
    189 			res.sendHeader(20, 'text/gemini')
    190 			res.end(dat)
    191 		})
    192 		return
    193 	}
    194 
    195 	const indreq = req.path.match(/^\/([^/]+)\/?$/)
    196 	if (indreq) {
    197 		try {
    198 			const gmi = await make_index_request(indreq[1], "/" + indreq[1])
    199 			res.sendHeader(20, 'text/gemini')
    200 			return res.end(gmi)
    201 		} catch (e) {
    202 			if (e.status === 404)
    203 				return res.notFound()
    204 			else
    205 				return crashed(res, e)
    206 		}
    207 	}
    208 
    209 	const pathreq = req.path.match(/\/([^/]+)\/(.*)$/)
    210 	if (!pathreq) return res.gone()
    211 
    212 	const host = pathreq[1]
    213 	const slug = pathreq[2]
    214 
    215 	const url = host + "/" + slug + ".json"
    216 	try {
    217 		const gmi = await make_request(url, "/" + host)
    218 		res.sendHeader(20, 'text/gemini')
    219 		return res.end(gmi)
    220 	} catch (e) {
    221 		if (e.status === 404)
    222 			return res.notFound()
    223 		else
    224 			return crashed(res, e)
    225 	}
    226 }
    227 
    228 function crashed(res, err)
    229 {
    230 	console.log(err)
    231 	res.sendHeader(40)
    232 	return res.end()
    233 }
    234 
    235 function serve(opts)
    236 {
    237 	// opts = { cert, key, passphrase }
    238 	return createServer(opts, handle_request)
    239 }
    240 
    241 module.exports = serve