Porównaj commity

...

2 Commity

Autor SHA1 Wiadomość Data
Candid Dauth 4bfe3ff4c2 Add iframely link tags 2024-03-21 05:46:27 +01:00
Candid Dauth e8798f3cde Attempt to implement oembed API 2024-03-21 04:50:35 +01:00
9 zmienionych plików z 142 dodań i 40 usunięć

Wyświetl plik

@ -1,7 +1,6 @@
<script setup lang="ts">
import Icon from "./ui/icon.vue";
import type { FindPadsResult } from "facilmap-types";
import decodeURIComponent from "decode-uri-component";
import { computed, ref } from "vue";
import { useToasts } from "./ui/toasts/toasts.vue";
import Pagination from "./ui/pagination.vue";
@ -11,6 +10,7 @@
import { injectContextRequired, requireClientContext, requireMapContext } from "./facil-map-context-provider/facil-map-context-provider.vue";
import type { FacilMapContext } from "./facil-map-context-provider/facil-map-context";
import ValidatedField from "./ui/validated-form/validated-field.vue";
import { parsePadUrl } from "facilmap-utils";
const toasts = useToasts();
@ -20,15 +20,9 @@
const ITEMS_PER_PAGE = 20;
function parsePadId(val: string, context: FacilMapContext): { padId: string; hash: string } {
if (val.startsWith(context.baseUrl))
val = decodeURIComponent(val.substr(context.baseUrl.length));
const hashIdx = val.indexOf("#");
if (hashIdx == -1)
return { padId: val, hash: "" };
else
return { padId: val.substr(0, hashIdx), hash: val.substr(hashIdx) };
function parsePadId(val: string, context: FacilMapContext): { padId: string; hash: string } | undefined {
const url = val.startsWith(context.baseUrl) ? val : `${context.baseUrl}${val}`;
return parsePadUrl(url, context.baseUrl);
}
const context = injectContextRequired();
@ -50,18 +44,22 @@
const url = computed(() => {
const parsed = parsePadId(padId.value, context);
return context.baseUrl + encodeURIComponent(parsed.padId) + parsed.hash;
if (parsed) {
return context.baseUrl + encodeURIComponent(parsed.padId) + parsed.hash;
}
});
function handleSubmit(): void {
const parsed = parsePadId(padId.value, context);
client.value.openPad(parsed.padId);
modalRef.value!.modal.hide();
if (parsed) {
client.value.openPad(parsed.padId);
modalRef.value!.modal.hide();
setTimeout(() => {
// TODO: This is called too early
mapContext.value.components.hashHandler.applyHash(parsed.hash);
}, 0);
setTimeout(() => {
// TODO: This is called too early
mapContext.value.components.hashHandler.applyHash(parsed.hash);
}, 0);
}
}
function openResult(result: FindPadsResult): void {
@ -106,15 +104,19 @@
function validatePadIdFormat(padId: string) {
const parsed = parsePadId(padId, context);
if (parsed.padId.includes("/")) {
if (!parsed) {
return "Please enter a valid map ID or URL.";
}
}
async function validatePadExistence(padId: string) {
const padInfo = await client.value.getPad({ padId });
if (!padInfo) {
return "No map with this ID could be found.";
const parsed = parsePadId(padId, context);
if (parsed) {
const padInfo = await client.value.getPad({ padId: parsed.padId });
if (!padInfo) {
return "No map with this ID could be found.";
}
}
}
</script>

Wyświetl plik

@ -2,8 +2,8 @@
<html class="fm-facilmap-map">
<head>
<meta charset="utf-8">
<title><%= padData && padData.name ? `${padData.name} – ` : ''%><%=appName%></title>
<meta name="description" content="<%= padData && padData.description || "A fully-featured OpenStreetMap-based map where markers and lines can be added with live collaboration." %>" />
<title><%=normalizePageTitle(padData ? normalizePadName(padData.name) : undefined, appName)%></title>
<meta name="description" content="<%=normalizePageDescription(padData?.description)%>" />
<% if(!padData || (isReadOnly && padData.searchEngines)) { -%>
<meta name="robots" content="index,nofollow" />
<% } else { -%>
@ -16,6 +16,8 @@
<link rel="apple-touch-icon" href="<%=paths.base%>static/app-180.png">
<link rel="manifest" href="<%=paths.base%>manifest.json">
<link rel="search" type="application/opensearchdescription+xml" title="<%=appName%>" href="<%=paths.base%>opensearch.xml">
<link rel="alternate" type="application/json+oembed" href="<%=paths.base%>oembed?url=<%=encodeURIComponent(url)%>&amp;format=json" title="<%=normalizePageTitle(padData ? normalizePadName(padData.name) : undefined, appName)%>" />
<link rel="iframely geolocation web-share resizable" href="<%=url%>">
<style type="text/css">
html, body, #app {
margin: 0;

Wyświetl plik

@ -2,14 +2,14 @@
<html class="fm-facilmap-table">
<head>
<meta charset="utf-8">
<title><%=normalizePadName(padData.name)%> – <%=appName%></title>
<title><%=normalizePageTitle(normalizePadName(padData.name), appName)%></title>
<base href="../" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<%
if(!padData || padData.searchEngines) {
-%>
<meta name="robots" content="index,nofollow" />
<meta name="description" content="<%= padData && padData.description || "A fully-featured OpenStreetMap-based map where markers and lines can be added with live collaboration." %>" />
<meta name="description" content="<%=normalizePageDescription(padData.description)%>" />
<%
} else {
-%>
@ -21,6 +21,8 @@
<link rel="mask-icon" href="<%=paths.base%>favicon.svg" color="#00272a">
<link rel="apple-touch-icon" href="<%=paths.base%>static/app-180.png">
<link rel="manifest" href="<%=paths.base%>static/manifest.json">
<link rel="alternate" type="application/json+oembed" href="<%=paths.base%>oembed?url=<%=encodeURIComponent(url)%>&amp;format=json" title="<%=normalizePageTitle(normalizePadName(padData.name), appName)%>" />
<link rel="iframely resizable" href="<%=url%>">
<style type="text/css">
<% for (const path of styles) { -%>

Wyświetl plik

@ -91,17 +91,22 @@ export function createSingleTable(
})());
}
export function createTable(database: Database, padId: PadId, filter: string | undefined, hide: string[]): ReadableStream<string> {
export function createTable(database: Database, padId: PadId, filter: string | undefined, hide: string[], url: string): ReadableStream<string> {
return streamPromiseToStream((async () => {
const [padData, types] = await Promise.all([
database.pads.getPadData(padId),
asyncIteratorToArray(database.types.getTypes(padId))
]);
if (!padData) {
throw new Error(`Pad with read-only ID ${padId} could not be found.`);
}
return renderTable({
padData,
types,
renderSingleTable: (typeId, params) => createSingleTable(database, padId, typeId, filter, hide, params)
renderSingleTable: (typeId, params) => createSingleTable(database, padId, typeId, filter, hide, params),
url
});
})());
}

Wyświetl plik

@ -5,7 +5,7 @@ import type { ID, PadData, Type } from "facilmap-types";
import * as ejs from "ejs";
import { Router, type RequestHandler } from "express";
import { static as expressStatic } from "express";
import { normalizePadName, type InjectedConfig, quoteHtml } from "facilmap-utils";
import { normalizePadName, type InjectedConfig, quoteHtml, normalizePageTitle, normalizePageDescription } from "facilmap-utils";
import config from "./config";
import { streamPromiseToStream, streamReplace } from "./utils/streams";
import { ReadableStream } from "stream/web";
@ -70,6 +70,7 @@ function getInjectedConfig(): InjectedConfig {
export interface RenderMapParams {
padData: Pick<PadData, "name" | "description" | "searchEngines"> | undefined;
isReadOnly: boolean;
url: string;
}
export async function renderMap(params: RenderMapParams): Promise<string> {
@ -82,16 +83,20 @@ export async function renderMap(params: RenderMapParams): Promise<string> {
appName: config.appName,
config: getInjectedConfig(),
hasCustomCssFile: !!config.customCssFile,
normalizePageTitle,
normalizePageDescription,
normalizePadName,
...injections,
paths,
...params
});
}
export function renderTable({ padData, types, renderSingleTable }: {
padData: PadData | undefined;
export function renderTable({ padData, types, renderSingleTable, url }: {
padData: PadData;
types: Type[];
renderSingleTable: (typeId: ID, params: TableParams) => ReadableStream<string>;
url: string;
}): ReadableStream<string> {
return streamPromiseToStream((async () => {
const [template, injections] = await Promise.all([
@ -106,6 +111,8 @@ export function renderTable({ padData, types, renderSingleTable }: {
hasCustomCssFile: !!config.customCssFile,
paths,
normalizePadName,
normalizePageTitle,
normalizePageDescription,
quoteHtml,
renderSingleTable: (typeId: ID, params: TableParams) => {
const placeholder = `%${generateRandomId(32)}%`;
@ -114,6 +121,7 @@ export function renderTable({ padData, types, renderSingleTable }: {
},
padData,
types,
url
});
return streamReplace(rendered, replace);
@ -153,4 +161,20 @@ export async function getOpensearchXml(baseUrl: string): Promise<string> {
appName: config.appName,
baseUrl
});
}
export function getOembedJson(baseUrl: string, padData: PadData | undefined, params: { url: string; maxwidth?: number; maxheight?: number }): string {
const width = params.maxwidth ?? 800;
const height = params.maxheight ?? 500;
return JSON.stringify({
"title": normalizePageTitle(padData ? normalizePadName(padData.name) : undefined, config.appName),
"type": "rich",
"height": height,
"width": width,
"version": "1.0",
"provider_name": config.appName,
"provider_url": baseUrl,
"html": `<iframe style="height:${height}px; width:${width}px; border:none;" src="${quoteHtml(params.url)}"></iframe>`
});
}

Wyświetl plik

@ -1,15 +1,15 @@
import compression from "compression";
import express, { type Request, type Response } from "express";
import { createServer, type Server as HttpServer } from "http";
import { stringifiedIdValidator, type PadId } from "facilmap-types";
import { stringifiedIdValidator, type PadData, type PadId } from "facilmap-types";
import { createSingleTable, createTable } from "./export/table.js";
import Database from "./database/database";
import { exportGeoJson } from "./export/geojson.js";
import { exportGpx, exportGpxZip } from "./export/gpx.js";
import domainMiddleware from "express-domain-middleware";
import { Readable, Writable } from "stream";
import { getOpensearchXml, getPwaManifest, getStaticFrontendMiddleware, renderMap, type RenderMapParams } from "./frontend";
import { getSafeFilename, normalizePadName } from "facilmap-utils";
import { getOembedJson, getOpensearchXml, getPwaManifest, getStaticFrontendMiddleware, renderMap, type RenderMapParams } from "./frontend";
import { getSafeFilename, normalizePadName, parsePadUrl } from "facilmap-utils";
import { paths } from "facilmap-frontend/build.js";
import config from "./config";
import { exportCsv } from "./export/csv.js";
@ -21,6 +21,8 @@ function getBaseUrl(req: Request): string {
export async function initWebserver(database: Database, port: number, host?: string): Promise<HttpServer> {
const padMiddleware = async (req: Request<{ padId: string }>, res: Response<string>) => {
const baseUrl = getBaseUrl(req);
let params: RenderMapParams;
if(req.params?.padId) {
const padData = await database.pads.getPadDataByAnyId(req.params.padId);
@ -31,7 +33,8 @@ export async function initWebserver(database: Database, port: number, host?: str
name: normalizePadName(padData.name),
description: padData.description
},
isReadOnly: padData.id === req.params.padId
isReadOnly: padData.id === req.params.padId,
url: `${baseUrl}${encodeURIComponent(req.params.padId)}`
};
} else {
res.status(404);
@ -41,13 +44,15 @@ export async function initWebserver(database: Database, port: number, host?: str
name: "",
description: ""
},
isReadOnly: true
isReadOnly: true,
url: `${baseUrl}${encodeURIComponent(req.params.padId)}`
};
}
} else {
params = {
padData: undefined,
isReadOnly: true
isReadOnly: true,
url: baseUrl
};
}
@ -83,10 +88,43 @@ export async function initWebserver(database: Database, port: number, host?: str
}
});
app.use("/_app/static/sw.js", (req, res, next) => {
app.use(`${paths.base}static/sw.js`, (req, res, next) => {
res.setHeader("Service-Worker-Allowed", "/");
next();
});
app.use(`${paths.base}oembed`, async (req, res, next) => {
const query = z.object({
url: z.string(),
maxwidth: z.number().optional(),
maxheight: z.number().optional(),
format: z.string().optional()
}).parse(req.query);
if (query.format != null && query.format !== "json") {
res.status(501).send();
return;
}
const baseUrl = getBaseUrl(req);
let padData: PadData | undefined;
if (query.url === baseUrl || `${query.url}/` === baseUrl) {
padData = undefined;
} else {
const parsed = parsePadUrl(query.url, baseUrl);
if (parsed) {
padData = await database.pads.getPadDataByAnyId(parsed.padId);
} else {
res.status(404).send();
return;
}
}
res.header("Content-type", "application/json");
res.send(getOembedJson(baseUrl, padData, query));
});
app.use(await getStaticFrontendMiddleware());
// If no file with this name has been found, we render a pad
@ -129,6 +167,7 @@ export async function initWebserver(database: Database, port: number, host?: str
filter: z.string().optional(),
hide: z.string().optional()
}).parse(req.query);
const baseUrl = getBaseUrl(req);
res.type("html");
res.setHeader("Referrer-Policy", "origin");
@ -136,7 +175,8 @@ export async function initWebserver(database: Database, port: number, host?: str
database,
req.params.padId,
query.filter,
query.hide ? query.hide.split(',') : []
query.hide ? query.hide.split(',') : [],
`${baseUrl}${encodeURIComponent(req.params.padId)}/table`
).pipeTo(Writable.toWeb(res));
});

Wyświetl plik

@ -34,6 +34,7 @@
],
"dependencies": {
"cheerio": "^1.0.0-rc.12",
"decode-uri-component": "^0.4.1",
"domhandler": "^5.0.3",
"dompurify": "^3.0.9",
"facilmap-types": "workspace:^",

Wyświetl plik

@ -1,4 +1,5 @@
import { cloneDeep, isEqual } from "lodash-es";
import decodeURIComponent from "decode-uri-component";
const LETTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const LENGTH = 12;
@ -96,8 +97,10 @@ export function getObjectDiff(obj1: Record<keyof any, any>, obj2: Record<keyof a
export function decodeQueryString(str: string): Record<string, string> {
const obj: Record<string, string> = { };
for(const segment of str.replace(/^\?/, "").split(/[;&]/)) {
const pair = segment.split("=");
obj[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);
if (segment !== "") {
const pair = segment.split("=");
obj[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] ?? "");
}
}
return obj;
}
@ -146,6 +149,14 @@ export function normalizeLineName(name: string | undefined): string {
return name || "Untitled line";
}
export function normalizePageTitle(padName: string | undefined, appName: string): string {
return `${padName ? `${padName}` : ''}${appName}`;
}
export function normalizePageDescription(padDescription: string | undefined): string {
return padDescription || "A fully-featured OpenStreetMap-based map where markers and lines can be added with live collaboration.";
}
/**
* Performs a 3-way merge. Takes the difference between oldObject and newObject and applies it to targetObject.
* @param oldObject {Object}
@ -166,4 +177,18 @@ export function mergeObject<T extends Record<keyof any, any>>(oldObject: T | und
export function getSafeFilename(fname: string): string {
return fname.replace(/[\\/:*?"<>|]+/g, '_');
}
export function parsePadUrl(url: string, baseUrl: string): { padId: string; hash: string } | undefined {
if (url.startsWith(baseUrl)) {
const m = url.slice(baseUrl.length).match(/^([^/]+)(\/table)?(\?|#|$)/);
if (m) {
const hashIdx = url.indexOf("#");
return {
padId: decodeURIComponent(m[1]),
hash: hashIdx === -1 ? "" : url.substr(hashIdx)
};
}
}
}

Wyświetl plik

@ -3846,6 +3846,7 @@ __metadata:
"@types/jsdom": ^21.1.6
"@types/linkifyjs": ^2.1.7
cheerio: ^1.0.0-rc.12
decode-uri-component: ^0.4.1
domhandler: ^5.0.3
dompurify: ^3.0.9
facilmap-types: "workspace:^"