commit
6fc2098741
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "txtdot",
|
"name": "txtdot",
|
||||||
"version": "1.1.1",
|
"version": "1.2.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "txtdot",
|
"name": "txtdot",
|
||||||
"version": "1.1.1",
|
"version": "1.2.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/static": "^6.10.2",
|
"@fastify/static": "^6.10.2",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "txtdot",
|
"name": "txtdot",
|
||||||
"version": "1.1.1",
|
"version": "1.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "dist/app.js",
|
"main": "dist/app.js",
|
||||||
|
34
src/errors/api.ts
Normal file
34
src/errors/api.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
export interface IApiError {
|
||||||
|
code: number;
|
||||||
|
name: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const errorSchema = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
code: {
|
||||||
|
type: "number",
|
||||||
|
description: "HTTP error code",
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: "string",
|
||||||
|
description: "Exception class name",
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
type: "string",
|
||||||
|
description: "Exception message",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const errorResponseSchema = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
data: {
|
||||||
|
type: "object",
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
|
error: errorSchema,
|
||||||
|
},
|
||||||
|
};
|
@ -1,14 +1,66 @@
|
|||||||
import { FastifyReply, FastifyRequest } from "fastify";
|
import { FastifyReply, FastifyRequest } from "fastify";
|
||||||
import { NotHtmlMimetypeError } from "./main";
|
import { NotHtmlMimetypeError, TxtDotError } from "./main";
|
||||||
|
import { getFastifyError } from "./validation";
|
||||||
|
|
||||||
export default function errorHandler(
|
export default function errorHandler(
|
||||||
error: Error,
|
error: Error,
|
||||||
_: FastifyRequest,
|
req: FastifyRequest,
|
||||||
reply: FastifyReply
|
reply: FastifyReply
|
||||||
) {
|
) {
|
||||||
|
if (req.originalUrl.startsWith("/api/")) {
|
||||||
|
return apiErrorHandler(error, reply);
|
||||||
|
}
|
||||||
|
return htmlErrorHandler(error, reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
function apiErrorHandler(error: Error, reply: FastifyReply) {
|
||||||
|
function generateResponse(code: number) {
|
||||||
|
return reply.code(code).send({
|
||||||
|
data: null,
|
||||||
|
error: {
|
||||||
|
code: code,
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof NotHtmlMimetypeError) {
|
||||||
|
return generateResponse(501);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getFastifyError(error)?.statusCode === 400) {
|
||||||
|
return generateResponse(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof TxtDotError) {
|
||||||
|
return generateResponse(error.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
return generateResponse(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function htmlErrorHandler(error: Error, reply: FastifyReply) {
|
||||||
if (error instanceof NotHtmlMimetypeError) {
|
if (error instanceof NotHtmlMimetypeError) {
|
||||||
return reply.redirect(301, error.url);
|
return reply.redirect(301, error.url);
|
||||||
} else {
|
|
||||||
return error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (getFastifyError(error)?.statusCode === 400) {
|
||||||
|
return reply.code(400).view("/templates/error.ejs", {
|
||||||
|
code: 400,
|
||||||
|
description: `Invalid parameter specified: ${error.message}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof TxtDotError) {
|
||||||
|
return reply.code(error.code).view("/templates/error.ejs", {
|
||||||
|
code: error.code,
|
||||||
|
description: error.description,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return reply.code(500).view("/templates/error.ejs", {
|
||||||
|
code: 500,
|
||||||
|
description: `${error.name}: ${error.message}`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,34 @@
|
|||||||
export class EngineParseError extends Error {}
|
export abstract class TxtDotError extends Error {
|
||||||
export class InvalidParameterError extends Error {}
|
code: number;
|
||||||
export class LocalResourceError extends Error {}
|
name: string;
|
||||||
export class NotHtmlMimetypeError extends Error {
|
description: string;
|
||||||
url: string;
|
|
||||||
constructor(params: { url: string }) {
|
constructor(code: number, name: string, description: string) {
|
||||||
super();
|
super(description);
|
||||||
this.url = params?.url;
|
this.code = code;
|
||||||
|
this.name = name;
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EngineParseError extends TxtDotError {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(422, "EngineParseError", `Parse error: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LocalResourceError extends TxtDotError {
|
||||||
|
constructor() {
|
||||||
|
super(403, "LocalResourceError", "Proxying local resources is forbidden.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotHtmlMimetypeError extends Error {
|
||||||
|
name: string = "NotHtmlMimetypeError";
|
||||||
|
url: string;
|
||||||
|
|
||||||
|
constructor(url: string) {
|
||||||
|
super();
|
||||||
|
this.url = url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
9
src/errors/validation.ts
Normal file
9
src/errors/validation.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export interface IFastifyValidationError {
|
||||||
|
statusCode?: number;
|
||||||
|
code?: string;
|
||||||
|
validation?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFastifyError(error: Error) {
|
||||||
|
return error as unknown as IFastifyValidationError;
|
||||||
|
}
|
@ -6,16 +6,15 @@ import { DOMWindow } from "jsdom";
|
|||||||
|
|
||||||
import readability from "./readability";
|
import readability from "./readability";
|
||||||
import google from "./google";
|
import google from "./google";
|
||||||
|
import stackoverflow from "./stackoverflow/main";
|
||||||
|
|
||||||
import { generateProxyUrl } from "../utils/generate";
|
import { generateProxyUrl } from "../utils/generate";
|
||||||
import isLocalResource from "../utils/islocal";
|
import isLocalResource from "../utils/islocal";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
InvalidParameterError,
|
|
||||||
LocalResourceError,
|
LocalResourceError,
|
||||||
NotHtmlMimetypeError,
|
NotHtmlMimetypeError,
|
||||||
} from "../errors/main";
|
} from "../errors/main";
|
||||||
import stackoverflow from "./stackoverflow/main";
|
|
||||||
|
|
||||||
export default async function handlePage(
|
export default async function handlePage(
|
||||||
url: string, // remote URL
|
url: string, // remote URL
|
||||||
@ -28,21 +27,21 @@ export default async function handlePage(
|
|||||||
throw new LocalResourceError();
|
throw new LocalResourceError();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (engine && engineList.indexOf(engine) === -1) {
|
|
||||||
throw new InvalidParameterError("Invalid engine");
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await axios.get(url);
|
const response = await axios.get(url);
|
||||||
const mime: string | undefined = response.headers["content-type"]?.toString();
|
const mime: string | undefined = response.headers["content-type"]?.toString();
|
||||||
|
|
||||||
if (mime && mime.indexOf("text/html") === -1) {
|
if (mime && mime.indexOf("text/html") === -1) {
|
||||||
throw new NotHtmlMimetypeError({ url });
|
throw new NotHtmlMimetypeError(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
const window = new JSDOM(response.data, { url }).window;
|
const window = new JSDOM(response.data, { url }).window;
|
||||||
|
|
||||||
[...window.document.getElementsByTagName("a")].forEach((link) => {
|
[...window.document.getElementsByTagName("a")].forEach((link) => {
|
||||||
link.href = generateProxyUrl(requestUrl, link.href, engine);
|
try {
|
||||||
|
link.href = generateProxyUrl(requestUrl, link.href, engine);
|
||||||
|
} catch (_err) {
|
||||||
|
// ignore TypeError: Invalid URL
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (engine) {
|
if (engine) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
export default {
|
export default {
|
||||||
version: "1.1.0",
|
version: "1.1.1",
|
||||||
description:
|
description:
|
||||||
"HTTP proxy that parses only text, links and pictures from pages reducing internet traffic, removing ads and heavy scripts",
|
"HTTP proxy that parses only text, links and pictures from pages reducing internet traffic, removing ads and heavy scripts",
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { FastifyInstance } from "fastify";
|
import { FastifyInstance } from "fastify";
|
||||||
|
|
||||||
import { GetSchema, IGetSchema } from "../types/requests";
|
import { GetSchema, IGetSchema } from "../types/requests/browser";
|
||||||
import handlePage from "../handlers/main";
|
import handlePage from "../handlers/main";
|
||||||
import { generateRequestUrl } from "../utils/generate";
|
import { generateRequestUrl } from "../utils/generate";
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { FastifyInstance } from "fastify";
|
import { FastifyInstance } from "fastify";
|
||||||
import { engineList } from "../handlers/main";
|
import { engineList } from "../handlers/main";
|
||||||
import { indexSchema } from "../types/requests";
|
import { indexSchema } from "../types/requests/browser";
|
||||||
|
|
||||||
export default async function indexRoute(fastify: FastifyInstance) {
|
export default async function indexRoute(fastify: FastifyInstance) {
|
||||||
fastify.get("/", { schema: indexSchema }, async (_, reply) => {
|
fastify.get("/", { schema: indexSchema }, async (_, reply) => {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { EngineRequest, IParseSchema, parseSchema } from "../types/requests";
|
|
||||||
import { FastifyInstance } from "fastify";
|
import { FastifyInstance } from "fastify";
|
||||||
|
|
||||||
|
import { EngineRequest, IParseSchema, parseSchema } from "../types/requests/api";
|
||||||
|
|
||||||
import handlePage from "../handlers/main";
|
import handlePage from "../handlers/main";
|
||||||
import { generateRequestUrl } from "../utils/generate";
|
import { generateRequestUrl } from "../utils/generate";
|
||||||
|
|
||||||
@ -8,15 +10,18 @@ export default async function parseRoute(fastify: FastifyInstance) {
|
|||||||
"/api/parse",
|
"/api/parse",
|
||||||
{ schema: parseSchema },
|
{ schema: parseSchema },
|
||||||
async (request: EngineRequest) => {
|
async (request: EngineRequest) => {
|
||||||
return await handlePage(
|
return {
|
||||||
request.query.url,
|
data: await handlePage(
|
||||||
generateRequestUrl(
|
request.query.url,
|
||||||
request.protocol,
|
generateRequestUrl(
|
||||||
request.hostname,
|
request.protocol,
|
||||||
request.originalUrl
|
request.hostname,
|
||||||
|
request.originalUrl
|
||||||
|
),
|
||||||
|
request.query.engine
|
||||||
),
|
),
|
||||||
request.query.engine
|
error: null,
|
||||||
);
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { FastifyInstance } from "fastify";
|
import { FastifyInstance } from "fastify";
|
||||||
|
|
||||||
import { GetRequest, IParseSchema, rawHtmlSchema } from "../types/requests";
|
import { IParseSchema, rawHtmlSchema } from "../types/requests/api";
|
||||||
|
import { GetRequest } from "../types/requests/browser";
|
||||||
|
|
||||||
import handlePage from "../handlers/main";
|
import handlePage from "../handlers/main";
|
||||||
import { generateRequestUrl } from "../utils/generate";
|
import { generateRequestUrl } from "../utils/generate";
|
||||||
|
|
||||||
|
@ -1,99 +0,0 @@
|
|||||||
import { FastifyRequest, FastifySchema } from "fastify";
|
|
||||||
import { handlerSchema } from "../handlers/handler.interface";
|
|
||||||
import { engineList } from "../handlers/main";
|
|
||||||
|
|
||||||
export type GetRequest = FastifyRequest<{
|
|
||||||
Querystring: {
|
|
||||||
url: string;
|
|
||||||
format?: string;
|
|
||||||
engine?: string;
|
|
||||||
};
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export interface IGetQuery {
|
|
||||||
url: string;
|
|
||||||
format?: string;
|
|
||||||
engine?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IParseQuery {
|
|
||||||
url: string;
|
|
||||||
engine?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IGetSchema {
|
|
||||||
Querystring: IGetQuery;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IParseSchema {
|
|
||||||
Querystring: IParseQuery;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const indexSchema = {
|
|
||||||
produces: ["text/html"],
|
|
||||||
hide: true
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getQuerySchema = {
|
|
||||||
type: "object",
|
|
||||||
required: ["url"],
|
|
||||||
properties: {
|
|
||||||
url: {
|
|
||||||
type: "string",
|
|
||||||
description: "URL",
|
|
||||||
},
|
|
||||||
format: {
|
|
||||||
type: "string",
|
|
||||||
enum: ["text", "html", ""],
|
|
||||||
default: "html",
|
|
||||||
},
|
|
||||||
engine: {
|
|
||||||
type: "string",
|
|
||||||
enum: [...engineList, ""],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const parseQuerySchema = {
|
|
||||||
type: "object",
|
|
||||||
required: ["url"],
|
|
||||||
properties: {
|
|
||||||
url: {
|
|
||||||
type: "string",
|
|
||||||
description: "URL",
|
|
||||||
},
|
|
||||||
engine: {
|
|
||||||
type: "string",
|
|
||||||
enum: [...engineList, ""],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GetSchema: FastifySchema = {
|
|
||||||
description: "Get page",
|
|
||||||
hide: true,
|
|
||||||
querystring: getQuerySchema,
|
|
||||||
produces: ["text/html", "text/plain"],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const parseSchema: FastifySchema = {
|
|
||||||
description: "Parse page",
|
|
||||||
querystring: parseQuerySchema,
|
|
||||||
response: {
|
|
||||||
"2xx": handlerSchema,
|
|
||||||
},
|
|
||||||
produces: ["text/json"],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const rawHtmlSchema: FastifySchema = {
|
|
||||||
description: "Get raw HTML",
|
|
||||||
querystring: parseQuerySchema,
|
|
||||||
produces: ["text/html"],
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EngineRequest = FastifyRequest<{
|
|
||||||
Querystring: {
|
|
||||||
url: string;
|
|
||||||
engine?: string;
|
|
||||||
};
|
|
||||||
}>;
|
|
66
src/types/requests/api.ts
Normal file
66
src/types/requests/api.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { FastifySchema, FastifyRequest } from "fastify";
|
||||||
|
import { IApiError, errorResponseSchema } from "../../errors/api";
|
||||||
|
import { handlerSchema } from "../../handlers/handler.interface";
|
||||||
|
import { engineList } from "../../handlers/main";
|
||||||
|
|
||||||
|
export interface IApiResponse<T> {
|
||||||
|
data?: T;
|
||||||
|
error?: IApiError;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IParseQuery {
|
||||||
|
url: string;
|
||||||
|
engine?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IParseSchema {
|
||||||
|
Querystring: IParseQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parseQuerySchema = {
|
||||||
|
type: "object",
|
||||||
|
required: ["url"],
|
||||||
|
properties: {
|
||||||
|
url: {
|
||||||
|
type: "string",
|
||||||
|
description: "URL",
|
||||||
|
},
|
||||||
|
engine: {
|
||||||
|
type: "string",
|
||||||
|
enum: [...engineList, ""],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseSchema: FastifySchema = {
|
||||||
|
description: "Parse the page and get all data from the engine",
|
||||||
|
querystring: parseQuerySchema,
|
||||||
|
response: {
|
||||||
|
"2xx": {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
data: handlerSchema,
|
||||||
|
error: {
|
||||||
|
type: "object",
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"4xx": errorResponseSchema,
|
||||||
|
"5xx": errorResponseSchema,
|
||||||
|
},
|
||||||
|
produces: ["text/json"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const rawHtmlSchema: FastifySchema = {
|
||||||
|
description: "Parse the page and get raw HTML from the engine",
|
||||||
|
querystring: parseQuerySchema,
|
||||||
|
produces: ["text/html"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EngineRequest = FastifyRequest<{
|
||||||
|
Querystring: {
|
||||||
|
url: string;
|
||||||
|
engine?: string;
|
||||||
|
};
|
||||||
|
}>;
|
48
src/types/requests/browser.ts
Normal file
48
src/types/requests/browser.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { FastifyRequest, FastifySchema } from "fastify";
|
||||||
|
import { engineList } from "../../handlers/main";
|
||||||
|
|
||||||
|
export type GetRequest = FastifyRequest<{
|
||||||
|
Querystring: IGetQuery;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export interface IGetQuery {
|
||||||
|
url: string;
|
||||||
|
format?: string;
|
||||||
|
engine?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGetSchema {
|
||||||
|
Querystring: IGetQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const indexSchema = {
|
||||||
|
produces: ["text/html"],
|
||||||
|
hide: true
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getQuerySchema = {
|
||||||
|
type: "object",
|
||||||
|
required: ["url"],
|
||||||
|
properties: {
|
||||||
|
url: {
|
||||||
|
type: "string",
|
||||||
|
description: "URL",
|
||||||
|
},
|
||||||
|
format: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["text", "html", ""],
|
||||||
|
default: "html",
|
||||||
|
},
|
||||||
|
engine: {
|
||||||
|
type: "string",
|
||||||
|
enum: [...engineList, ""],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GetSchema: FastifySchema = {
|
||||||
|
description: "Get page",
|
||||||
|
hide: true,
|
||||||
|
querystring: getQuerySchema,
|
||||||
|
produces: ["text/html", "text/plain"],
|
||||||
|
};
|
@ -9,8 +9,10 @@
|
|||||||
--bg2: #bbb;
|
--bg2: #bbb;
|
||||||
--fg2: #333;
|
--fg2: #333;
|
||||||
|
|
||||||
--accent: hsl(207, 100%, 40%);
|
--accent: #0070cc; /* hsl(207, 100%, 40%) */
|
||||||
--accent-hl: hsl(207, 100%, 20%);
|
--accent-hl: #003866; /* hsl(207, 100%, 20%) */
|
||||||
|
|
||||||
|
--error: #ff9400;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
@ -21,8 +23,8 @@
|
|||||||
--bg2: #444;
|
--bg2: #444;
|
||||||
--fg2: #bbb;
|
--fg2: #bbb;
|
||||||
|
|
||||||
--accent: hsl(207, 100%, 60%);
|
--accent: #33a3ff; /* hsl(207, 100%, 60%) */
|
||||||
--accent-hl: hsl(207, 100%, 80%);
|
--accent-hl: #99d1ff; /* hsl(207, 100%, 80%) */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
60
static/form.css
Normal file
60
static/form.css
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
.input-grid {
|
||||||
|
display: grid;
|
||||||
|
/* 2 columns: auto width, min-content width */
|
||||||
|
grid-template-columns: auto min-content;
|
||||||
|
|
||||||
|
/* gap: row column */
|
||||||
|
gap: 0.5rem 0.25rem;
|
||||||
|
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#url {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%; /* shrink to #submit height */
|
||||||
|
|
||||||
|
outline: none;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 0.125rem solid var(--fg2);
|
||||||
|
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
#url::placeholder {
|
||||||
|
color: var(--fg2);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#submit {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 0.125rem solid var(--accent);
|
||||||
|
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
|
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
@ -5,9 +5,10 @@
|
|||||||
|
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
border-bottom: 0.125rem solid var(--bg2);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
@ -2,73 +2,17 @@ main {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
h1 > span {
|
|
||||||
|
h1 > .dot {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
h1 > .dot-err {
|
||||||
.input-grid {
|
color: var(--error);
|
||||||
display: grid;
|
|
||||||
/* 2 columns: auto width, min-content width */
|
|
||||||
grid-template-columns: auto min-content;
|
|
||||||
|
|
||||||
/* gap: row column */
|
|
||||||
gap: 0.5rem 0.25rem;
|
|
||||||
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#url {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%; /* shrink to #submit height */
|
|
||||||
|
|
||||||
outline: none;
|
|
||||||
border: 0;
|
|
||||||
border-bottom: 0.125rem solid var(--fg2);
|
|
||||||
|
|
||||||
background: var(--bg);
|
|
||||||
color: var(--fg);
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
#url::placeholder {
|
|
||||||
color: var(--fg2);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#submit {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
border: 0;
|
|
||||||
border-bottom: 0.125rem solid var(--accent);
|
|
||||||
|
|
||||||
background: var(--bg);
|
|
||||||
color: var(--fg);
|
|
||||||
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
}
|
||||||
|
21
templates/error.ejs
Normal file
21
templates/error.ejs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
<title>txt. <%= code %></title>
|
||||||
|
<link rel="stylesheet" href="/static/common.css">
|
||||||
|
<link rel="stylesheet" href="/static/index.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<h1>txt<span class="dot-err">.</span></h1>
|
||||||
|
<p><%= description %></p>
|
||||||
|
</header>
|
||||||
|
<a href="/" class="button secondary">Home</a>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -15,10 +15,9 @@
|
|||||||
<a class="button secondary" href="/">Home</a>
|
<a class="button secondary" href="/">Home</a>
|
||||||
<a class="button secondary" href="<%= remoteUrl %>">Original page</a>
|
<a class="button secondary" href="<%= remoteUrl %>">Original page</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="title">
|
<p class="title">
|
||||||
<%= parsed.title %>
|
<%= parsed.title %>
|
||||||
</div>
|
</p>
|
||||||
<hr>
|
|
||||||
<%- parsed.content %>
|
<%- parsed.content %>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
@ -9,11 +9,12 @@
|
|||||||
<title>txt. main page</title>
|
<title>txt. main page</title>
|
||||||
<link rel="stylesheet" href="/static/common.css">
|
<link rel="stylesheet" href="/static/common.css">
|
||||||
<link rel="stylesheet" href="/static/index.css">
|
<link rel="stylesheet" href="/static/index.css">
|
||||||
|
<link rel="stylesheet" href="/static/form.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
<header>
|
<header>
|
||||||
<h1>txt<span>.</span></h1>
|
<h1>txt<span class="dot">.</span></h1>
|
||||||
<p><%= desc %></p>
|
<p><%= desc %></p>
|
||||||
</header>
|
</header>
|
||||||
<form action="/get" method="get" class="input-grid">
|
<form action="/get" method="get" class="input-grid">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user