Hashish culture/language/hashish.md
also
hash, haschisch, hachisch, hasch, hashish, hachís, jachís
sources
Wiktionary — hachís (Spanish), including jachís 9 unchanged
Changelog 2268d4ctree
1 atlas entry edited12 files of code, design or docs +367 −249 lines
hash, haschisch, hachisch, hasch, hashish, hachís, jachís
Wiktionary — hachís (Spanish), including jachís 9 unchanged
README.md +6 @@ -82,2 +82,8 @@ counts are published. - Data from others keeps its own terms, listed on the [data page](https://cannabisfreedom.fyi/data/), which also explains the downloads and how to cite. + +`/api/glossary.json` publishes the glossary, language-atlas spellings and country `words` with +source references. Its versioned `recognition` section contains cannabis-related spellings and +inflection prefixes for consumers such as the Cannabis Enforcement Tracker. Ambiguous everyday +words are identified separately; general legal glossary terms are not cannabis keyword matches. +This vocabulary is a recognition aid, not an exhaustive language classifier or evidence of an event.
package-lock.json +279 Not shown: a picture, a generated file, or past the length shown per change.
package.json +8 @@ -27,5 +27,5 @@ }, "dependencies": { - "@astrojs/markdown-remark": "^7.3.2", + "@astrojs/markdown-remark": "^7.3.5", "@fontsource-variable/inter": "^5.3.0", "@fontsource-variable/jetbrains-mono": "^5.3.0", @@ -34,5 +34,5 @@ "@fontsource/special-elite": "^5.3.0", "@iconify-json/iconoir": "^1.2.12", - "astro": "^7.3.5", + "astro": "^7.4.4", "astro-icon": "^1.2.0", "d3-geo": "^3.1.1", @@ -51,8 +51,12 @@ "@fontsource/jetbrains-mono": "^5.3.0", "@resvg/resvg-js": "^2.6.2", - "flag-icons": "^7.5.0", - "js-yaml": "^4.3.2", + "flag-icons": "^7.6.0", + "js-yaml": "^5.5.0", "playwright-core": "^1.64.1", "x-satori": "^0.5.1" + }, + "allowScripts": { + "esbuild@0.28.2": true, + "fsevents@2.3.3": true } }
scripts/answers.test.mjs +14 @@ -11,5 +11,5 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import yaml from "js-yaml"; +import * as yaml from "js-yaml"; import { answersFor } from "../src/lib/answers.js"; import { SEGMENT_IDS } from "../src/lib/segment-ids.js"; @@ -135,2 +135,15 @@ test('custodial enforcement band does not manufacture an imprisonment frequency assert.equal(placeOf('kh').result.caught.label, 'Prison'); }); + +// The public glossary also feeds the enforcement tracker's multilingual source check. +test("glossary API exports sourced multilingual cannabis vocabulary and inflection rules", async () => { + const { glossaryAPI } = await import("../src/lib/glossary-api.js"); + const data = glossaryAPI(path.resolve(root, "..")); + assert.equal(data.version, 1); + assert.ok(data.entries.some(e => e.kind === "glossary" && e.id === "THC")); + assert.ok(data.entries.some(e => e.kind === "country-word" && e.word === "konopa" && e.sources.length)); + for (const word of ["marihuana", "konopí", "大麻", "กัญชา"]) assert.ok(data.recognition.words.includes(word), word); + assert.ok(data.recognition.prefixes.includes("konop")); + assert.ok(data.recognition.prefixes.includes("marihuan")); + assert.ok(!data.recognition.words.includes("decriminalised")); +});
scripts/osm-shops.mjs +1 @@ -16,5 +16,5 @@ import fs from "node:fs"; import { geoContains, geoBounds, geoDistance, geoCentroid } from "d3-geo"; import { feature } from "topojson-client"; -import yaml from "js-yaml"; +import * as yaml from "js-yaml"; const SERVERS = ["https://overpass-api.de/api/interpreter", "https://overpass.private.coffee/api/interpreter", "https://maps.mail.ru/osm/tools/overpass/api/interpreter"];
scripts/profile-claims.test.mjs +1 @@ -36,5 +36,5 @@ test("no profile, article or history entry repeats a sentence known to be false" }); -import yaml from 'js-yaml'; +import * as yaml from 'js-yaml'; const country = (id) => yaml.load(fs.readFileSync(path.join(root, 'countries', `${id}.yaml`), 'utf8')); const events = () => yaml.load(fs.readFileSync(path.join(root, 'changes.yaml'), 'utf8'));
scripts/sync-tracker.mjs +1 @@ -10,5 +10,5 @@ import fs from "node:fs"; import path from "node:path"; -import yaml from "js-yaml"; +import * as yaml from "js-yaml"; const tracker = path.resolve(process.argv[2] || process.env.TRACKER_DIR || "../cannabis-freedom-index-tracker");
src/lib/gitlog.js +1 @@ -12,5 +12,5 @@ import { execFileSync } from "node:child_process"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import path from "node:path"; -import yaml from "js-yaml"; +import * as yaml from "js-yaml"; import { diffWords } from "diff";
src/lib/glossary-api.js +46 @@ -0,0 +1,46 @@ +// Public vocabulary from the same sourced glossary, atlas and country data as the site. +import fs from "node:fs"; +import path from "node:path"; +import * as yaml from "js-yaml"; + +const frontmatter = (file) => yaml.load(fs.readFileSync(file, "utf8").match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1] ?? "") ?? {}; +const clean = (value) => String(value ?? "").replace(/\s*\([^)]*\)$/, "").trim(); +const ambiguous = new Set(["pot", "grass", "high", "joint", "flower", "tea", "j", "jay", "dope", "accession", "hybrid", "bang", "resin", "résine"]); +const cannabisGlossary = new Set(["prensado", "chira", "THC", "HHC", "cannabinoid", "edibles", "concentrates"]); + +export function glossaryAPI(root = process.cwd()) { + const entries = [], words = new Set(); + const add = (forms) => { + for (const value of forms) { + const word = clean(value); + if (word.length > 1 && !ambiguous.has(word.toLowerCase())) words.add(word); + } + }; + const glossary = yaml.load(fs.readFileSync(path.join(root, "glossary.yaml"), "utf8")) ?? []; + for (const item of glossary) { + entries.push({ kind: "glossary", ...item }); + if (cannabisGlossary.has(item.id)) add([item.id, ...(item.also ?? [])]); + } + const dir = path.join(root, "culture/language"); + for (const file of fs.readdirSync(dir).filter(f => f.endsWith(".md")).sort()) { + const item = frontmatter(path.join(dir, file)); + entries.push({ kind: "language", id: file.slice(0, -3), title: item.title, also: item.also ?? [], spelling: item.spelling ?? [], sources: item.sources ?? [], url: `/culture/language/${file.slice(0, -3)}/` }); + add([item.title, ...(item.also ?? []), ...(item.spelling ?? []).flatMap(s => [s.text, ...(!/^[\[/]/.test(s.say ?? "") ? [s.say] : [])])]); + } + function countries(dir, prefix = "") { + for (const file of fs.readdirSync(dir, { withFileTypes: true }).sort((a,b) => a.name.localeCompare(b.name))) { + if (file.isDirectory()) { countries(path.join(dir, file.name), prefix + file.name + "/"); continue; } + if (!file.name.endsWith(".yaml")) continue; + const item = yaml.load(fs.readFileSync(path.join(dir, file.name), "utf8")); + for (const word of item?.words ?? []) { + entries.push({ kind: "country-word", country: (prefix + file.name.slice(0,-5)).replace(/\/index$/, ""), ...word, sources: (word.sources ?? []).map(id => ({ id, ...item.sources?.[id] })) }); + add([word.word]); + } + } + } + countries(path.join(root, "countries")); + // Productive inflections: konopy/konopí/konopím, marihuanu/marihuany, etc. + // A prefix matches only at a Unicode word start; it never matches inside another word. + const prefixes = ["konop", "marihuan", "marijuan", "mariguan", "cannabi", "kanabi", "коноп", "марихуан", "гашиш"]; + return { version: 1, entries, recognition: { words: [...words].sort((a,b) => a.localeCompare(b)), prefixes, ambiguous: [...ambiguous].sort() } }; +}
src/lib/survey.js +1 @@ -2,5 +2,5 @@ // results (survey/results-<year>.json, written by scripts/survey-mock.cjs or survey-collect.cjs). // The form and the results page are both built from these two files. -import yaml from "js-yaml"; +import * as yaml from "js-yaml"; import raw from "../../survey/2026.yaml?raw"; import results from "../../survey/results-2026.json";
src/lib/terms.js +1 @@ -10,5 +10,5 @@ import { readFileSync, readdirSync } from "node:fs"; import path from "node:path"; -import yaml from "js-yaml"; +import * as yaml from "js-yaml"; const ROOT = process.cwd();
src/pages/api/glossary.json.js +5 @@ -0,0 +1,5 @@ +import { glossaryAPI } from "../../lib/glossary-api.js"; + +export function GET() { + return new Response(JSON.stringify(glossaryAPI()), { headers: { "Content-Type": "application/json; charset=utf-8" } }); +}