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