...
This commit is contained in:
		@@ -1,5 +1,3 @@
 | 
			
		||||
import path from "path";
 | 
			
		||||
import fs from "fs";
 | 
			
		||||
import http from "http";
 | 
			
		||||
import { URL } from "url";
 | 
			
		||||
import querystring from "querystring";
 | 
			
		||||
@@ -10,90 +8,26 @@ import Tpl from "./template.mjs";
 | 
			
		||||
export { Router, Tpl };
 | 
			
		||||
 | 
			
		||||
export default class flummpress {
 | 
			
		||||
  #mimes;
 | 
			
		||||
  #server;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.router = new Router();
 | 
			
		||||
    this.tpl = new Tpl();
 | 
			
		||||
 | 
			
		||||
    this.middleware = new Set();
 | 
			
		||||
    return this;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  use(obj) {
 | 
			
		||||
    if(obj instanceof Router) {
 | 
			
		||||
      this.router.routes = new Map([ ...this.router.routes, ...obj.routes ]);
 | 
			
		||||
      this.router.sortRoutes();
 | 
			
		||||
      this.router.use(obj);
 | 
			
		||||
    }
 | 
			
		||||
    else if(obj instanceof Tpl) {
 | 
			
		||||
      this.tpl = obj;
 | 
			
		||||
    }
 | 
			
		||||
    return this;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static({ dir = path.resolve() + "/public", route = /^\/public/ }) {
 | 
			
		||||
    if(!this.#mimes) {
 | 
			
		||||
      this.#mimes = new Map();
 | 
			
		||||
      (fs.readFileSync("./mime.types", "utf-8"))
 | 
			
		||||
        .split("\n")
 | 
			
		||||
        .filter(e => !e.startsWith("#") && e)
 | 
			
		||||
        .map(e => e.split(/\s{2,}/))
 | 
			
		||||
        .filter(e => e.length > 1)
 | 
			
		||||
        .forEach(m => m[1].split(" ").forEach(ext => this.#mimes.set(ext, m[0])));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.router.get(route, (req, res) => {
 | 
			
		||||
      try {
 | 
			
		||||
        const filename = req.url.pathname.replace(route, "") || "index.html";
 | 
			
		||||
        const mime = this.#mimes.get(filename.split(".").pop());
 | 
			
		||||
        const file = path.join(dir, filename);
 | 
			
		||||
        let stat;
 | 
			
		||||
        try {
 | 
			
		||||
          stat = fs.statSync(file);
 | 
			
		||||
        } catch {
 | 
			
		||||
          res.reply({
 | 
			
		||||
            code: 404,
 | 
			
		||||
            body: "404 - file not found."
 | 
			
		||||
          });
 | 
			
		||||
          return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if(!mime.startsWith("video") && !mime.startsWith("audio")) {
 | 
			
		||||
          res.reply({
 | 
			
		||||
            type: this.#mimes.get(filename.split(".").pop()).toLowerCase(),
 | 
			
		||||
            body: fs.readFileSync(path.join(dir, filename))
 | 
			
		||||
          });
 | 
			
		||||
          return this;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        if(req.headers.range) {
 | 
			
		||||
          const parts = req.headers.range.replace(/bytes=/, "").split("-");
 | 
			
		||||
          const start = parseInt(parts[0], 10);
 | 
			
		||||
          const end = parts[1] ? parseInt(parts[1], 10) : stat.size - 1;
 | 
			
		||||
          res.writeHead(206, {
 | 
			
		||||
            "Content-Range": `bytes ${start}-${end}/${stat.size}`,
 | 
			
		||||
            "Accept-Ranges": "bytes",
 | 
			
		||||
            "Content-Length": (end - start) + 1,
 | 
			
		||||
            "Content-Type": mime,
 | 
			
		||||
          });
 | 
			
		||||
          const stream = fs.createReadStream(file, { start: start, end: end })
 | 
			
		||||
            .on("open", () => stream.pipe(res))
 | 
			
		||||
            .on("error", err => res.end(err));
 | 
			
		||||
        }
 | 
			
		||||
    else {
 | 
			
		||||
          res.writeHead(200, {
 | 
			
		||||
            "Content-Length": stat.size,
 | 
			
		||||
            "Content-Type": mime,
 | 
			
		||||
          });
 | 
			
		||||
          fs.createReadStream(file).pipe(res);
 | 
			
		||||
      if(!this.middleware.has(obj))
 | 
			
		||||
        this.middleware.add(obj);
 | 
			
		||||
    }
 | 
			
		||||
      } catch(err) {
 | 
			
		||||
        console.error(err);
 | 
			
		||||
        res.reply({
 | 
			
		||||
          code: 500,
 | 
			
		||||
          body: "500 - f0ck hat keinen b0ck"
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    return this;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@@ -109,6 +43,16 @@ export default class flummpress {
 | 
			
		||||
        qs: {...querystring.parse(_url.search.substring(1))} // legacy
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      req.cookies = {};
 | 
			
		||||
      if(req.headers.cookie) {
 | 
			
		||||
        req.headers.cookie.split("; ").forEach(c => {
 | 
			
		||||
          const parts = c.split('=');
 | 
			
		||||
          req.cookies[parts.shift().trim()] = decodeURI(parts.join('='));
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      await Promise.all([...this.middleware].map(m => m(req, res)));
 | 
			
		||||
 | 
			
		||||
      const method = req.method.toLowerCase();
 | 
			
		||||
      const route = this.router.getRoute(req.url.pathname, req.method);
 | 
			
		||||
 | 
			
		||||
@@ -166,6 +110,10 @@ export default class flummpress {
 | 
			
		||||
    }) => res.writeHead(code, { "Content-Type": `${type}; charset=utf-8` }).end(body);
 | 
			
		||||
    res.json = msg => res.reply({ type: "application/json", body: msg });
 | 
			
		||||
    res.html = msg => res.reply({ type: "text/html", body: msg });
 | 
			
		||||
    res.redirect = (target, code = 307) => res.writeHead(code, {
 | 
			
		||||
      "Cache-Control": "no-cache, public",
 | 
			
		||||
      "Location": target
 | 
			
		||||
    }).end();
 | 
			
		||||
    return res;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,27 @@
 | 
			
		||||
import fs from "fs";
 | 
			
		||||
import path from "path";
 | 
			
		||||
import Tpl from "./template.mjs";
 | 
			
		||||
 | 
			
		||||
export default class Router {
 | 
			
		||||
  #mimes;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.routes = new Map();
 | 
			
		||||
    return this;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  async importRoutesFromPath(p, tpl = false) {
 | 
			
		||||
    await Promise.all(
 | 
			
		||||
      fs.readdirSync(path.resolve() + "/" + p)
 | 
			
		||||
        .filter(r => r.endsWith(".mjs"))
 | 
			
		||||
        .map(async r => {
 | 
			
		||||
          const tmp = (await import(`${path.resolve()}/${p}/${r}`)).default(this, tpl);
 | 
			
		||||
          this.use(tmp);
 | 
			
		||||
        })
 | 
			
		||||
    );
 | 
			
		||||
    return this;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  group(path, cb) {
 | 
			
		||||
    const methods = {
 | 
			
		||||
      get:  this.get.bind(this),
 | 
			
		||||
@@ -25,6 +43,16 @@ export default class Router {
 | 
			
		||||
    return this;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  use(obj) {
 | 
			
		||||
    if(obj instanceof Router) {
 | 
			
		||||
      this.routes = new Map([ ...this.routes, ...obj.routes ]);
 | 
			
		||||
      this.sortRoutes();
 | 
			
		||||
    }
 | 
			
		||||
    if(obj instanceof Tpl) {
 | 
			
		||||
      this.tpl = obj;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get(path, ...args) {
 | 
			
		||||
    if(args.length === 1)
 | 
			
		||||
      this.registerRoute(path, args[0], "get");
 | 
			
		||||
@@ -65,4 +93,75 @@ export default class Router {
 | 
			
		||||
    this.routes = new Map([...this.routes.entries()].sort().reverse());
 | 
			
		||||
    return this;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  readMimes(file = "/etc/mime.types") {
 | 
			
		||||
    this.#mimes = new Map();
 | 
			
		||||
    (fs.readFileSync(file, "utf-8"))
 | 
			
		||||
      .split("\n")
 | 
			
		||||
      .filter(e => !e.startsWith("#") && e)
 | 
			
		||||
      .map(e => e.split(/\s{2,}/))
 | 
			
		||||
      .filter(e => e.length > 1)
 | 
			
		||||
      .forEach(m => m[1].split(" ").forEach(ext => this.#mimes.set(ext, m[0])));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static({ dir = path.resolve() + "/public", route = /^\/public/ }) {
 | 
			
		||||
    if(!this.#mimes)
 | 
			
		||||
      this.readMimes();
 | 
			
		||||
 | 
			
		||||
    this.get(route, (req, res) => {
 | 
			
		||||
      try {
 | 
			
		||||
        const filename = req.url.pathname.replace(route, "") || "index.html";
 | 
			
		||||
        const mime = this.#mimes.get(filename.split(".").pop());
 | 
			
		||||
        const file = path.join(dir, filename);
 | 
			
		||||
        let stat;
 | 
			
		||||
        try {
 | 
			
		||||
          stat = fs.statSync(file);
 | 
			
		||||
        } catch(err) {
 | 
			
		||||
          console.log(err);
 | 
			
		||||
          res.reply({
 | 
			
		||||
            code: 404,
 | 
			
		||||
            body: "404 - file not found."
 | 
			
		||||
          });
 | 
			
		||||
          return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if(!mime.startsWith("video") && !mime.startsWith("audio")) {
 | 
			
		||||
          res.reply({
 | 
			
		||||
            type: this.#mimes.get(filename.split(".").pop()).toLowerCase(),
 | 
			
		||||
            body: fs.readFileSync(path.join(dir, filename))
 | 
			
		||||
          });
 | 
			
		||||
          return this;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        if(req.headers.range) {
 | 
			
		||||
          const parts = req.headers.range.replace(/bytes=/, "").split("-");
 | 
			
		||||
          const start = parseInt(parts[0], 10);
 | 
			
		||||
          const end = parts[1] ? parseInt(parts[1], 10) : stat.size - 1;
 | 
			
		||||
          res.writeHead(206, {
 | 
			
		||||
            "Content-Range": `bytes ${start}-${end}/${stat.size}`,
 | 
			
		||||
            "Accept-Ranges": "bytes",
 | 
			
		||||
            "Content-Length": (end - start) + 1,
 | 
			
		||||
            "Content-Type": mime,
 | 
			
		||||
          });
 | 
			
		||||
          const stream = fs.createReadStream(file, { start: start, end: end })
 | 
			
		||||
            .on("open", () => stream.pipe(res))
 | 
			
		||||
            .on("error", err => res.end(err));
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
          res.writeHead(200, {
 | 
			
		||||
            "Content-Length": stat.size,
 | 
			
		||||
            "Content-Type": mime,
 | 
			
		||||
          });
 | 
			
		||||
          fs.createReadStream(file).pipe(res);
 | 
			
		||||
        }
 | 
			
		||||
      } catch(err) {
 | 
			
		||||
        console.error(err);
 | 
			
		||||
        res.reply({
 | 
			
		||||
          code: 500,
 | 
			
		||||
          body: "500 - internal server error"
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    return this;
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										105
									
								
								src/template.mjs
									
									
									
									
									
								
							
							
						
						
									
										105
									
								
								src/template.mjs
									
									
									
									
									
								
							@@ -2,38 +2,108 @@ import fs from "fs";
 | 
			
		||||
import path from "path";
 | 
			
		||||
 | 
			
		||||
export default class {
 | 
			
		||||
  #views;
 | 
			
		||||
  #globals;
 | 
			
		||||
  #cache;
 | 
			
		||||
  #templates;
 | 
			
		||||
  #debug;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.views = path.resolve() + "/views";
 | 
			
		||||
    this.#views = "./views";
 | 
			
		||||
    this.#globals = {};
 | 
			
		||||
    this.#debug = false;
 | 
			
		||||
    this.#cache = true;
 | 
			
		||||
    this.#templates = new Map();
 | 
			
		||||
  };
 | 
			
		||||
  getTemplate(tpl) {
 | 
			
		||||
    return fs.readFileSync(`${this.views}/${tpl}.html`, "utf-8");
 | 
			
		||||
 | 
			
		||||
  set debug(d) {
 | 
			
		||||
    this.#debug = !!d;
 | 
			
		||||
  };
 | 
			
		||||
  set views(v) {
 | 
			
		||||
    this.#views = v;
 | 
			
		||||
    this.readdir(v);
 | 
			
		||||
  };
 | 
			
		||||
  set globals(g) {
 | 
			
		||||
    this.#globals = g;
 | 
			
		||||
  };
 | 
			
		||||
  set cache(c) {
 | 
			
		||||
    this.#cache = !!c;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  readdir(dir, root = dir) {
 | 
			
		||||
    for(const d of fs.readdirSync(`${path.resolve()}/${dir}`)) {
 | 
			
		||||
      if(d.endsWith(".html")) { // template
 | 
			
		||||
        const file = path.parse(`${dir.replace(this.#views, '')}/${d}`);
 | 
			
		||||
        const t_dir = file.dir.substring(1);
 | 
			
		||||
        const t_file = file.name;
 | 
			
		||||
        this.getTemplate(!t_dir ? t_file : `${t_dir}/${t_file}`);
 | 
			
		||||
      }
 | 
			
		||||
  render(tpl, data = {}) {
 | 
			
		||||
      else { // directory
 | 
			
		||||
        this.readdir(`${dir}/${d}`, root);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getTemplate(tpl) {
 | 
			
		||||
    let template = {};
 | 
			
		||||
    let cache = false;
 | 
			
		||||
    if(this.#cache && this.#templates.has(tpl)) {
 | 
			
		||||
      template = this.#templates.get(tpl);
 | 
			
		||||
      cache = template.cached;
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      template = {
 | 
			
		||||
        code: fs.readFileSync(`${this.#views}/${tpl}.html`, "utf-8"),
 | 
			
		||||
        cached: new Date()
 | 
			
		||||
      };
 | 
			
		||||
      this.#templates.set(tpl, template);
 | 
			
		||||
    }
 | 
			
		||||
    return [
 | 
			
		||||
      template.code,
 | 
			
		||||
      (this.#debug ? `<!-- ${tpl}.html ` + (cache ? `cached ${template.cached}` : `not cached`) + " -->" : "")
 | 
			
		||||
    ].join("");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  render(file, data = {}, locals) {
 | 
			
		||||
    data = { ...data, ...this.#globals, ...locals };
 | 
			
		||||
    try {
 | 
			
		||||
      const code = 'with(_data){const __html = [];' +
 | 
			
		||||
        '__html.push(\`' +
 | 
			
		||||
        tpl.replace(/[\t]/g, ' ')
 | 
			
		||||
          .replace(/'(?=[^\{]*}})/, '\t')
 | 
			
		||||
        this.getTemplate(file)
 | 
			
		||||
          .replace(/[\t]/g, ' ')
 | 
			
		||||
          .split('\`').join('\\\`')
 | 
			
		||||
          .split('\t').join('\`')
 | 
			
		||||
          .replace(/{{=(.+?)}}/g, '\`,$1,\`')
 | 
			
		||||
          .replace(/{{-(.+?)}}/g, '\`,this.escape($1),\`')
 | 
			
		||||
          .replace(/{{include (.*?)}}/g, (_, inc) => this.render(this.getTemplate(inc), data))
 | 
			
		||||
          .replace(/{{each (?<key>.*) as (?<val>.*)}}/g, (_, key, val) => `\`);this.forEach(${key},(${val},key)=>{__html.push(\``)
 | 
			
		||||
          .replace(/{{\/each}}/g, "\`);});__html.push(`")
 | 
			
		||||
          .split('{{').join('\`);')
 | 
			
		||||
          .split('}}').join('__html.push(\`')
 | 
			
		||||
        + '\`);return __html.join(\'\').replace(/\\n\\s*\\n/g, "\\n");}';
 | 
			
		||||
 | 
			
		||||
          .replace(/{{--(.*?)--}}/g, "") // comments
 | 
			
		||||
          .replace(/{{(.+?)}}/g, '\`,$1,\`') // yield variable
 | 
			
		||||
          .replace(/{!!(.+?)!!}/g, '\`,this.escape($1),\`') // yield escaped variable
 | 
			
		||||
 | 
			
		||||
          .replace(/@js/g, "`);") // inject bare javascript
 | 
			
		||||
          .replace(/@endjs/g, ";__html.push(`")
 | 
			
		||||
 | 
			
		||||
          .replace(/@include\((.*?)\)/g, (_, inc) => this.render(inc, data)) // include template
 | 
			
		||||
 | 
			
		||||
          .replace(/@for\((.*?)\)$/gm, `\`);for($1){__html.push(\``)
 | 
			
		||||
          .replace(/@endfor/g, `\`);}__html.push(\``)
 | 
			
		||||
 | 
			
		||||
          .replace(/@each\((.*?) as (.*?)\)/g, `\`);this.forEach($1,($2,key)=>{__html.push(\``) // foreach for the poor
 | 
			
		||||
          .replace(/@endeach/g, "\`);});__html.push(`")
 | 
			
		||||
 | 
			
		||||
          .replace(/@elseif\((.*?)\)(\)?)/g, `\`);}else if($1$2){__html.push(\``) // if lol
 | 
			
		||||
          .replace(/@if\((.*?)\)(\)?)/g, `\`);if($1$2){__html.push(\``)
 | 
			
		||||
          .replace(/@else/g, "`);}else{__html.push(`")
 | 
			
		||||
          .replace(/@endif/g, "`);}__html.push(`") +
 | 
			
		||||
        '\`);return __html.join(\'\').replace(/\\n\\s*\\n/g, "\\n");}';
 | 
			
		||||
 | 
			
		||||
      return (new Function("_data", code)).bind({
 | 
			
		||||
        escape: this.escape,
 | 
			
		||||
        forEach: this.forEach
 | 
			
		||||
      })(data);
 | 
			
		||||
    } catch(err) {
 | 
			
		||||
      console.log(err);
 | 
			
		||||
      return err.message;
 | 
			
		||||
      console.log(file, err.message);
 | 
			
		||||
      return (this.#debug ? `${err.message} in ${file}` : '');
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  escape(str) {
 | 
			
		||||
    return (str + '')
 | 
			
		||||
      .replace(/&/g, '&')
 | 
			
		||||
@@ -45,6 +115,7 @@ export default class {
 | 
			
		||||
      .replace(/}/g, '}')
 | 
			
		||||
    ;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  forEach(o, f) {
 | 
			
		||||
    if(Array.isArray(o))
 | 
			
		||||
      o.forEach(f);
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user