diff --git a/components/Dictionary.vue b/components/Dictionary.vue
index 937a3ecf..7773f928 100644
--- a/components/Dictionary.vue
+++ b/components/Dictionary.vue
@@ -182,7 +182,7 @@
if (this.nounsRaw !== undefined) {
return;
}
- this.nounsRaw = await this.$axios.$get(`/nouns/all`);
+ this.nounsRaw = await this.$axios.$get(`/nouns`);
},
async setFilter(filter) {
this.filter = filter;
diff --git a/components/Footer.vue b/components/Footer.vue
index cbdb3517..d98e1719 100644
--- a/components/Footer.vue
+++ b/components/Footer.vue
@@ -37,12 +37,20 @@
-
-
-
- terms.header
-
-
+
+ -
+
+
+ terms.header
+
+
+ -
+
+
+ api.header
+
+
+
diff --git a/components/Login.vue b/components/Login.vue
index 3fa85514..945a91ec 100644
--- a/components/Login.vue
+++ b/components/Login.vue
@@ -87,8 +87,8 @@
return jwt.verify(this.token, process.env.PUBLIC_KEY, {
algorithm: 'RS256',
- audience: process.env.BASE_URL,
- issuer: process.env.BASE_URL,
+ audience: this.$base,
+ issuer: this.$base,
});
}
},
diff --git a/components/NounSubmitForm.vue b/components/NounSubmitForm.vue
index 2b670cee..0a40394b 100644
--- a/components/NounSubmitForm.vue
+++ b/components/NounSubmitForm.vue
@@ -118,7 +118,6 @@
diff --git a/routes/homepage.vue b/routes/homepage.vue
index 20a9d424..eb87c2c4 100644
--- a/routes/homepage.vue
+++ b/routes/homepage.vue
@@ -243,14 +243,14 @@
if (!this.selectedTemplate.pronoun()) {
return null;
}
- return this.addSlash(process.env.BASE_URL + '/' + (this.usedBaseEquals ? this.usedBase : this.longLink));
+ return this.addSlash(this.$base + '/' + (this.usedBaseEquals ? this.usedBase : this.longLink));
},
linkMultiple() {
if (!this.multiple.length) {
return null;
}
- return this.addSlash(process.env.BASE_URL + '/' + this.multiple.join('&'));
+ return this.addSlash(this.$base + '/' + this.multiple.join('&'));
},
sources() {
return getSources(this.selectedTemplate);
diff --git a/routes/profile.vue b/routes/profile.vue
index b8f6ba08..cddf34fa 100644
--- a/routes/profile.vue
+++ b/routes/profile.vue
@@ -141,7 +141,7 @@
for (let pronoun in this.profile.pronouns) {
if (!this.profile.pronouns.hasOwnProperty(pronoun)) { continue; }
- const link = pronoun.replace(new RegExp('^' + process.env.BASE_URL), '').replace(new RegExp('^/'), '');
+ const link = pronoun.replace(new RegExp('^' + this.$base), '').replace(new RegExp('^/'), '');
const template = buildTemplate(templates, link);
if (template) {
diff --git a/routes/profileEditor.vue b/routes/profileEditor.vue
index 8039ca4e..a26c0dea 100644
--- a/routes/profileEditor.vue
+++ b/routes/profileEditor.vue
@@ -188,7 +188,7 @@
this.$router.push(`/@${this.$user().username}`)
},
validatePronoun(pronoun) {
- const link = pronoun.replace(new RegExp('^' + process.env.BASE_URL), '').replace(new RegExp('^/'), '');
+ const link = pronoun.replace(new RegExp('^' + this.$base), '').replace(new RegExp('^/'), '');
const template = buildTemplate(templates, link);
return template ? null : 'profile.pronounsNotFound'
diff --git a/server/config.js b/server/config.js
deleted file mode 100644
index f90562b6..00000000
--- a/server/config.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import Suml from 'suml';
-const fs = require('fs');
-export default new Suml().parse(fs.readFileSync('./data/config.suml').toString());
diff --git a/server/index.js b/server/index.js
index e53aa389..f0a5ecb8 100644
--- a/server/index.js
+++ b/server/index.js
@@ -5,7 +5,7 @@ import session from 'express-session';
import cookieParser from 'cookie-parser';
import grant from "grant";
import router from "./routes/user";
-import config from './config';
+import { loadSuml } from './loader';
const app = express()
@@ -18,7 +18,7 @@ app.use(session({
}));
app.use(async function (req, res, next) {
- req.config = config;
+ req.config = loadSuml('config');
req.rawUser = authenticate(req);
req.user = req.rawUser && req.rawUser.authenticated ? req.rawUser : null;
req.admin = req.user && req.user.roles === 'admin';
@@ -34,8 +34,9 @@ app.use(require('./routes/user').default);
app.use(require('./routes/profile').default);
app.use(require('./routes/admin').default);
-app.use(require('./routes/nouns').default);
+app.use(require('./routes/templates').default);
app.use(require('./routes/sources').default);
+app.use(require('./routes/nouns').default);
export default {
path: '/api',
diff --git a/server/loader.js b/server/loader.js
new file mode 100644
index 00000000..9239adc9
--- /dev/null
+++ b/server/loader.js
@@ -0,0 +1,12 @@
+const fs = require('fs');
+import Suml from 'suml';
+import Papa from 'papaparse';
+
+export const loadSuml = name => new Suml().parse(fs.readFileSync(`./data/${name}.suml`).toString());
+
+export const loadTsv = name => Papa.parse(fs.readFileSync(`./data/${name}.tsv`).toString(), {
+ dynamicTyping: true,
+ header: true,
+ skipEmptyLines: true,
+ delimiter: '\t',
+}).data;
diff --git a/server/routes/banner.js b/server/routes/banner.js
index 13301747..e489e292 100644
--- a/server/routes/banner.js
+++ b/server/routes/banner.js
@@ -1,11 +1,13 @@
import { Router } from 'express';
import SQL from 'sql-template-strings';
import {createCanvas, loadImage, registerFont} from "canvas";
-import translations from "../translations";
+import { loadSuml } from '../loader';
import avatar from '../avatar';
import {buildTemplate, parseTemplates} from "../../src/buildTemplate";
import {loadTsv} from "../../src/tsv";
+const translations = loadSuml('translations');
+
const drawCircle = (context, image, x, y, size) => {
context.save();
context.beginPath();
diff --git a/server/routes/nouns.js b/server/routes/nouns.js
index d3daabc2..442500e1 100644
--- a/server/routes/nouns.js
+++ b/server/routes/nouns.js
@@ -23,7 +23,7 @@ const approve = async (db, id) => {
const router = Router();
-router.get('/nouns/all', async (req, res) => {
+router.get('/nouns', async (req, res) => {
return res.json(await req.db.all(SQL`
SELECT * FROM nouns
WHERE locale = ${req.config.locale}
@@ -32,6 +32,17 @@ router.get('/nouns/all', async (req, res) => {
`));
});
+router.get('/nouns/search/:term', async (req, res) => {
+ const term = '%' + req.params.term + '%';
+ return res.json(await req.db.all(SQL`
+ SELECT * FROM nouns
+ WHERE locale = ${req.config.locale}
+ AND approved >= ${req.admin ? 0 : 1}
+ AND (masc like ${term} OR fem like ${term} OR neutr like ${term} OR mascPl like ${term} OR femPl like ${term} OR neutrPl like ${term})
+ ORDER BY approved, masc
+ `));
+});
+
router.post('/nouns/submit', async (req, res) => {
if (!(req.user && req.user.admin) && isTroll(JSON.stringify(req.body))) {
return res.json('ok');
diff --git a/server/routes/sources.js b/server/routes/sources.js
index 5c639064..92442c94 100644
--- a/server/routes/sources.js
+++ b/server/routes/sources.js
@@ -1,6 +1,7 @@
import { Router } from 'express';
import mailer from "../../src/mailer";
-import {camelCase, capitalise} from "../../src/helpers";
+import { camelCase } from "../../src/helpers";
+import { loadTsv } from '../loader';
const generateId = title => {
return camelCase(title.split(' ').slice(0, 2));
@@ -21,8 +22,26 @@ const buildEmail = (data, user) => {
return `${tsv.join('\t')}
`;
}
+const loadSources = () => {
+ return loadTsv('sources/sources').map(s => {
+ if (s.author) {
+ s.author = s.author.replace('^', '');
+ }
+ s.fragments = s.fragments.split('@').map(f => f.replace(/\|/g, '\n'));
+ return s;
+ });
+}
+
const router = Router();
+router.get('/sources', async (req, res) => {
+ return res.json(loadSources());
+});
+
+router.get('/sources/:key', async (req, res) => {
+ return res.json([...loadSources().filter(s => s.key === req.params.key), null][0]);
+});
+
router.post('/sources/submit', async (req, res) => {
const emailBody = buildEmail(req.body, req.user);
diff --git a/server/routes/templates.js b/server/routes/templates.js
new file mode 100644
index 00000000..dcbc9fb2
--- /dev/null
+++ b/server/routes/templates.js
@@ -0,0 +1,56 @@
+import { Router } from 'express';
+import { loadTsv } from '../loader';
+import {buildTemplate, parseTemplates} from "../../src/buildTemplate";
+import {buildList} from "../../src/helpers";
+import {Example} from "../../src/classes";
+
+const buildExample = e => new Example(
+ Example.parse(e.singular),
+ Example.parse(e.plural || e.singular),
+ e.isHonorific,
+)
+
+const requestExamples = r => {
+ if (!r || !r.length) {
+ return loadTsv('templates/examples');
+ }
+
+ return buildList(function* () {
+ for (let rr of r) {
+ let [singular, plural, isHonorific] = rr.split('|');
+ yield { singular, plural, isHonorific: !!isHonorific};
+ }
+ });
+}
+
+const addExamples = (pronoun, examples) => {
+ return buildList(function* () {
+ for (let example of examples) {
+ yield buildExample(example).format(pronoun);
+ }
+ });
+}
+
+const router = Router();
+
+router.get('/pronouns', async (req, res) => {
+ const templates = parseTemplates(loadTsv('templates/templates'));
+ for (let template in templates) {
+ if (!templates.hasOwnProperty(template)) { continue; }
+ templates[template].examples = addExamples(templates[template], requestExamples(req.query.examples))
+ }
+ return res.json(templates);
+});
+
+router.get('/pronouns/:pronoun*', async (req, res) => {
+ const pronoun = buildTemplate(
+ parseTemplates(loadTsv('templates/templates')),
+ req.params.pronoun + req.params[0],
+ );
+ if (pronoun) {
+ pronoun.examples = addExamples(pronoun, requestExamples(req.query.examples))
+ }
+ return res.json(pronoun);
+});
+
+export default router;
diff --git a/server/routes/user.js b/server/routes/user.js
index 4b29bafb..5d520f5e 100644
--- a/server/routes/user.js
+++ b/server/routes/user.js
@@ -2,13 +2,15 @@ import { Router } from 'express';
import SQL from 'sql-template-strings';
import {ulid} from "ulid";
import {buildDict, makeId, now} from "../../src/helpers";
-import translations from "../translations";
import jwt from "../../src/jwt";
import mailer from "../../src/mailer";
-import config from '../config';
+import { loadSuml } from '../loader';
import avatar from '../avatar';
import { config as socialLoginConfig, handlers as socialLoginHandlers } from '../social';
+const config = loadSuml('config');
+const translations = loadSuml('translations');
+
const USERNAME_CHARS = 'A-Za-zĄĆĘŁŃÓŚŻŹąćęłńóśżź0-9._-';
const normalise = s => s.trim().toLowerCase();
diff --git a/server/translations.js b/server/translations.js
deleted file mode 100644
index 7eeeafe6..00000000
--- a/server/translations.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import Suml from 'suml';
-const fs = require('fs');
-export default new Suml().parse(fs.readFileSync('./data/translations.suml').toString());
diff --git a/src/classes.js b/src/classes.js
index 9450620c..e19839d2 100644
--- a/src/classes.js
+++ b/src/classes.js
@@ -6,14 +6,6 @@ export class ExamplePart {
this.variable = variable;
this.str = str;
}
-
- format(form) {
- if (!this.variable) {
- return this.str[form.plural];
- }
-
- return form[this.str[form.plural]];
- }
}
export class Example {
@@ -44,12 +36,12 @@ export class Example {
return parts;
}
- format(form) {
- return Example.ucfirst(this.parts.map(part => part.format(form)).join(''));
- }
+ format(pronoun) {
+ const plural = this.isHonorific ? pronoun.pluralHonorific[0] : pronoun.plural[0];
- static ucfirst(str) {
- return str[0].toUpperCase() + str.slice(1);
+ return capitalise(this[plural ? 'pluralParts' : 'singularParts'].map(part => {
+ return part.variable ? pronoun.getMorpheme(part.str) : part.str;
+ }).join(''));
}
}