generated from polymech/site-template
tests:filter/model - validate link cache
This commit is contained in:
parent
dd2e9b9fd3
commit
8620ee4817
@ -23,7 +23,7 @@
|
||||
"format": "unix-time"
|
||||
}
|
||||
],
|
||||
"default": "2025-03-28T10:00:39.789Z"
|
||||
"default": "2025-03-28T10:59:06.225Z"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -3,7 +3,7 @@
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Return a list of useful references (only with links), as Markdown, grouped : Articles, Books, Papers, Youtube, Opensource Designs, ... Dont comment !\n\nText to process:\nThis injection machine operates with a motor, reducing manual effort and increasing pressure for creating more detailed products.\n\n\nUser Location: Bogota, Colombia\n\nMachine Design: \nMotor Injection Machine\n\nMachine Size: \nHeight: 195 cm (76.8 in); Width: 50 cm (19.7 in); Depth: 50 cm (19.7 in)\n\nMachine Cost: \nColombia Bill of Materials: COP$4,700,000\n\nUnique Features: \nThis machine uses a motor to apply pressure, replacing the manual lever from earlier models. It is an upgrade to the Basic Injection Machine.\n\nCompatibility: \nSuitable for injection molds.\n\nPlastic Types: \nPP, HDPE, LDPE, PS\n\nTo construct this machine, you will require:\n\n- Turning (lathe machining)\n- Milling (mill machining)\n- General metalworking (cutting, drilling)\n- Welding\n- Advanced assembly (requires specific tools, measurement instruments, and knowledge of tolerances for alignment and assembly)\n- General electrical work (wiring safety switches, temperature controllers)\n- Motor electrical work (wiring motor, contactor, overload protection)\n\nWatch this video to learn how to build this machine:\n\n0:00 Preparation\n3:09 Motor Injection Machine Introduction\n3:36 Chapter I: Frame Construction\n7:12 Chapter II: Mould Area Construction\n8:25 Chapter III: Piston System Construction\n14:39 Chapter IV: Heating Barrel Construction\n17:51 Chapter V: Electrical Wiring\n18:56 Chapter VI: Motor Connection\n20:10 Chapter VII: Assembly\n\n### How to Use the Machine\n\n1. Power on the machine and fill the barrel with plastic.\n2. Wait 25 minutes for the first injection after powering on and filling.\n3. Position the mold on the jack surface, pressing it tightly against the nozzle.\n4. Activate the motor to lower the piston, pushing molten plastic into the mold until the belt slips on the pulley.\n5. Stop the motor and maintain piston pressure for approximately 5 seconds.\n6. Reverse the motor to raise the piston.\n7. Refill the barrel before removing the mold from the nozzle for continuous injections.\n8. Remove the mold by lowering the jack.\n9. Open the mold and extract the injected part.\n10. Close the mold and repeat from step 3.\n\n### Recommendations\n\nEnsure the molds have a conical nozzle connection or use an adapter to fit your mold nozzle. This machine generates sufficient pressure to inject products with very thin walls.\n\nHow to Build a Motor Injection Machine\n\nIf you are unable to build the machine or wish to purchase other machines or molds I offer, please visit my shop."
|
||||
"content": "Extract the required tools, software hardware from the following tutorial. Return as Markdown chapters (H3) with very short bullet points (not bold), with links, max. 5.\n\nText to process:\n# Tutorial: Building a Mini Press for Compression Moulding\n\nTo construct this mini press, you will need the following:\n\n- Welding machine\n- Access to a laser cutting machine\n- Drilling machine\n- Basic assembly skills\n\n\nUser Location: Liberec, Czechia\n\nAll steps are detailed in the video tutorial. Click the yellow download button above to access the blueprints and CAD files.\n\nThis standard size frame can press sheets measuring 37x37 cm (14.6x14.6 in).\n\nMaximum recommended mold height is 80 mm (3.15 in).\n\nProducts made include:\n\n- Sheets: 37x37 cm (14.6x14.6 in), with thicknesses of 3, 5, 20 mm (0.12, 0.20, 0.79 in)\n- Coasters\n- Clocks\n- Clipboards\n- Sheets later used for CNC cutting: lamp designs, animal models.\n\nComplete Machine:\n\nLaser-cut parts for pressing plates:\n\nExplore upgrades and tips for compression molding on:\n\n[Youtube and Instagram](https://linktr.ee/plastmakers)"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
|
||||
15
package.json
15
package.json
@ -12,6 +12,9 @@
|
||||
"astro": "astro",
|
||||
"generate-pwa-assets": "pwa-assets-generator --preset minimal-2023 public/logo.svg",
|
||||
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
||||
"lint": "eslint . --ext .ts,.tsx --fix",
|
||||
"lint:check": "eslint . --ext .ts,.tsx",
|
||||
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
|
||||
"test:lighthouse": "lighthouse https://polymech.io/en --output json --output html --output-path ./dist/reports/report.html --save-assets --chrome-flags=\"--window-size=1440,700 --headless\"",
|
||||
"test:debug": "playwright test",
|
||||
"test:ui": "playwright test --ui",
|
||||
@ -98,13 +101,23 @@
|
||||
"devDependencies": {
|
||||
"@types/google-publisher-tag": "^1.20250210.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/markdownlint": "^0.26.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
||||
"@typescript-eslint/parser": "^7.1.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"prettier": "^3.2.5",
|
||||
"@vitest/coverage-v8": "^1.3.1",
|
||||
"jest": "^29.7.0",
|
||||
"micromark-util-sanitize-uri": "^2.0.1",
|
||||
"normalize-url": "^8.0.1",
|
||||
"sass-embedded": "^1.83.4",
|
||||
"ts-jest": "^29.3.0",
|
||||
"vitest": "^1.3.1"
|
||||
"vitest": "^1.3.1",
|
||||
"markdownlint": "^0.39.0",
|
||||
"markdownlint-cli": "^0.39.0",
|
||||
"markdownlint-rule-helpers": "^0.19.0"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "ts-jest/presets/default-esm",
|
||||
|
||||
@ -45,16 +45,16 @@ describe('filters', () => {
|
||||
expect(renderLinks(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should replace blacklisted links with [Link Removed]', () => {
|
||||
it('should replace blacklisted links with empty string', () => {
|
||||
const input = 'Check out https://preciousplastic.com';
|
||||
expect(renderLinks(input)).toBe('Check out [Link Removed]');
|
||||
expect(renderLinks(input)).toBe('Check out ');
|
||||
});
|
||||
|
||||
it('should handle multiple links in text', () => {
|
||||
const input = 'Check out https://example.com and https://preciousplastic.com';
|
||||
const result = renderLinks(input);
|
||||
expect(result).toContain('example.com');
|
||||
expect(result).toContain('[Link Removed]');
|
||||
expect(result).toContain('and ');
|
||||
});
|
||||
});
|
||||
|
||||
@ -128,7 +128,7 @@ describe('filters', () => {
|
||||
it('should handle markdown links with blacklisted URLs', async () => {
|
||||
const input = 'Check out [example](https://preciousplastic.com)';
|
||||
const result = await applyFilters(input, default_filters_markdown);
|
||||
expect(result).toBe('Check out [Link Removed]');
|
||||
expect(result).toBe('Check out example');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -2,19 +2,14 @@ export * from './howto-model.js'
|
||||
import { HOWTO_ROOT } from "config/config.js";
|
||||
import { filterMarkdownLinks } from "../base/markdown.js";
|
||||
import { check } from 'linkinator';
|
||||
import { linkCache } from './link-cache.js';
|
||||
|
||||
// Types and interfaces
|
||||
interface Item {
|
||||
data: {
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FilterFunction { (text: string): string | Promise<string> }
|
||||
|
||||
// Constants
|
||||
export const item_path = (item: Item): string => `${HOWTO_ROOT()}/${item.data.slug}`;
|
||||
|
||||
export const blacklist: readonly string[] = [
|
||||
'precious-plastic',
|
||||
'fair-enough',
|
||||
@ -41,6 +36,7 @@ export const wordReplaceMap: Readonly<Record<string, string>> = {
|
||||
Car: "tufftuff"
|
||||
} as const;
|
||||
|
||||
export const item_path = (item: Item): string => `${HOWTO_ROOT()}/${item.data.slug}`;
|
||||
/**
|
||||
* Shortens a URL by removing 'www.' prefix and trailing slashes
|
||||
* @param url - The URL to shorten
|
||||
@ -58,6 +54,72 @@ export const shortenUrl = (url: string): string => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the domain name from a URL
|
||||
* @param url - The URL to extract domain from
|
||||
* @returns The domain name or empty string if invalid
|
||||
*/
|
||||
export const getDomain = (url: string): string => {
|
||||
try {
|
||||
const { hostname } = new URL(url);
|
||||
return hostname.replace(/^www\./, '');
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates links in text and removes invalid ones
|
||||
* @param text - The text containing links to validate
|
||||
* @returns Promise resolving to text with invalid links removed
|
||||
*/
|
||||
export const validateLinks = async (text: string): Promise<string> => {
|
||||
const urlRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
||||
const matches = text.matchAll(urlRegex);
|
||||
let processedText = text;
|
||||
|
||||
for (const match of matches) {
|
||||
const [fullMatch, linkText, url] = match;
|
||||
try {
|
||||
// Check cache first
|
||||
const cachedResult = await linkCache.get(url);
|
||||
if (cachedResult !== null) {
|
||||
if (!cachedResult) {
|
||||
processedText = processedText.replace(fullMatch, `~~${linkText}~~`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// For testing purposes, treat example.com URLs as valid
|
||||
if (url.includes('example.com')) {
|
||||
await linkCache.set(url, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Encode the URL to handle special characters
|
||||
const encodedUrl = encodeURI(url);
|
||||
console.log(`Checking link: ${encodedUrl}`);
|
||||
const result = await check({ path: encodedUrl,
|
||||
timeout: 2500,
|
||||
});
|
||||
|
||||
// Cache the result
|
||||
await linkCache.set(url, result.passed);
|
||||
|
||||
// Add strikethrough for invalid links
|
||||
if (!result.passed) {
|
||||
processedText = processedText.replace(fullMatch, `~~${linkText}~~`);
|
||||
}
|
||||
} catch (error) {
|
||||
// If there's an error checking the link, assume it's invalid
|
||||
await linkCache.set(url, false);
|
||||
processedText = processedText.replace(fullMatch, `~~${linkText}~~`);
|
||||
}
|
||||
}
|
||||
|
||||
return processedText;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders links in text, replacing blacklisted URLs with "[Link Removed]"
|
||||
* @param text - The text containing URLs to process
|
||||
@ -68,9 +130,11 @@ export const renderLinks = (text: string): string =>
|
||||
const isBlacklisted = urlBlacklist.some((domain) =>
|
||||
url.toLowerCase().includes(domain.toLowerCase())
|
||||
);
|
||||
return isBlacklisted
|
||||
? "[Link Removed]"
|
||||
: `<a class="text-orange-600 underline" href="${url}" target="_blank" rel="noopener noreferrer">${shortenUrl(url)}</a>`;
|
||||
if (isBlacklisted) return "";
|
||||
|
||||
const domain = getDomain(url);
|
||||
const displayText = `${domain}: ${shortenUrl(url)}`;
|
||||
return `<a class="text-orange-600 underline" href="${url}" target="_blank" rel="noopener noreferrer">${displayText}</a>`;
|
||||
});
|
||||
|
||||
/**
|
||||
@ -96,42 +160,6 @@ export const replaceWords = (text: string): string =>
|
||||
text
|
||||
);
|
||||
|
||||
/**
|
||||
* Validates links in text and removes invalid ones
|
||||
* @param text - The text containing links to validate
|
||||
* @returns Promise resolving to text with invalid links removed
|
||||
*/
|
||||
export const validateLinks = async (text: string): Promise<string> => {
|
||||
const urlRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
||||
const matches = text.matchAll(urlRegex);
|
||||
let processedText = text;
|
||||
|
||||
for (const match of matches) {
|
||||
const [fullMatch, linkText, url] = match;
|
||||
try {
|
||||
// For testing purposes, treat example.com URLs as valid
|
||||
if (url.includes('example.com')) {
|
||||
continue; // Keep the original markdown link
|
||||
}
|
||||
|
||||
// Encode the URL to handle special characters
|
||||
const encodedUrl = encodeURI(url);
|
||||
const result = await check({ path: encodedUrl });
|
||||
|
||||
// Remove markdown format only if the link check fails
|
||||
if (!result.passed) {
|
||||
processedText = processedText.replace(fullMatch, linkText);
|
||||
}
|
||||
// Valid links are left unchanged in their original markdown format
|
||||
} catch (error) {
|
||||
// If there's an error checking the link, assume it's invalid
|
||||
processedText = processedText.replace(fullMatch, linkText);
|
||||
}
|
||||
}
|
||||
|
||||
return processedText;
|
||||
};
|
||||
|
||||
export const default_filters_plain: FilterFunction[] = [
|
||||
renderLinks,
|
||||
filterBannedPhrases,
|
||||
@ -139,7 +167,7 @@ export const default_filters_plain: FilterFunction[] = [
|
||||
] as const;
|
||||
|
||||
export const default_filters_markdown: FilterFunction[] = [
|
||||
(text: string) => filterMarkdownLinks(text, urlBlacklist.map(url => ({ pattern: url, replacement: "[Link Removed]" }))),
|
||||
(text: string) => filterMarkdownLinks(text, urlBlacklist.map(url => ({ pattern: url, replacement: "" }))),
|
||||
filterBannedPhrases,
|
||||
replaceWords,
|
||||
validateLinks
|
||||
|
||||
Loading…
Reference in New Issue
Block a user