diff --git a/components/index.mjs b/components/index.mjs new file mode 100644 index 0000000..81d4665 --- /dev/null +++ b/components/index.mjs @@ -0,0 +1,34 @@ +//@ts-check +import {fetchText} from '../modules/fetchText.mjs' +import {parseFileList} from '../modules/parseFileList.mjs' +import {html} from '../modules/html.mjs' + +const indexListTemplate = html`

Hello World!

` + +class IndexList extends HTMLElement { + + static template = indexListTemplate + static observedAttributes = ["src"]; + + static define(tag = "index-list") { + customElements.define(tag, this) + } + + shadowRoot = this.shadowRoot || this.attachShadow({ mode: "open" }) + src = "" + + connectedCallback() { + this.shadowRoot.replaceChildren(IndexList.template.content.cloneNode(true)) + } + + attributeChangedCallback(name, oldValue, newValue) { + if (oldValue === newValue) { + return; + } + switch (name) { + case "src": + this.src = newValue + fetchText(newValue).then(parseFileList).then() + } + } +} \ No newline at end of file diff --git a/modules/generateDomFromString.mjs b/modules/generateDomFromString.mjs deleted file mode 100644 index 1a4800b..0000000 --- a/modules/generateDomFromString.mjs +++ /dev/null @@ -1,13 +0,0 @@ -//@ts-check - -/** - * Generates valid dom elements from a string - * @param {string} htmlString - */ -export const generateDomFromString = (htmlString) => - /** @type {HTMLElement} */ ( - new DOMParser().parseFromString(`
${htmlString}
`, "text/html") - .firstChild - ); - -export default generateDomFromString; diff --git a/modules/index.mjs b/modules/index.mjs index b87da6c..b461635 100644 --- a/modules/index.mjs +++ b/modules/index.mjs @@ -1,51 +1,5 @@ //@ts-check - -import { changeTitle } from "./changeTitle.mjs"; -import { documentMode } from "./documentMode.mjs"; -import { fetchMarkdown } from "./fetchMarkdown.mjs"; -import { fetchText } from "./fetchText.mjs"; -import { generateDomFromString } from "./generateDomFromString.mjs"; -import { getCurrentHashUrl } from "./getCurrentHashUrl.mjs"; -import { getElementById } from "./getElementById.mjs"; -import { isExternalUrl } from "./isExternalUrl.mjs"; -import { isLocalHost } from "./isLocalHost.mjs"; -import { isNotNull } from "./isNotNull.mjs"; -import { - onDocumentKeyUp, - onDocumentKeyDown, - onDocumentKey, -} from "./onDocumentKey.mjs"; -import { parseFileList } from "./parseFileList.mjs"; -import { - querySelectorDoc, - querySelectorParent, - querySelectorAll, -} from "./querySelectorAll.mjs"; -import { rewriteLocalUrls } from "./rewriteLocalUrls.mjs"; -import * as templateBlog from "./templateBlogUtils.mjs"; -import { wait } from "./wait.mjs"; -import { waitIfLocalHost } from "./waitIfLocalHost.mjs"; - -export { - changeTitle, - documentMode, - fetchMarkdown, - fetchText, - generateDomFromString, - getCurrentHashUrl, - getElementById, - isExternalUrl, - isLocalHost, - isNotNull, - onDocumentKeyUp, - onDocumentKeyDown, - onDocumentKey, - parseFileList, - querySelectorDoc, - querySelectorParent, - querySelectorAll, - rewriteLocalUrls, - templateBlog, - wait, - waitIfLocalHost, -}; +export * from './tickle/index.mjs' +export * from './utils/index.mjs' +export { default as blog } from './templateBlog.mjs' +export { default as musings } from './templateMusings.mjs' \ No newline at end of file diff --git a/modules/parseFileList.mjs b/modules/parseFileList.mjs deleted file mode 100644 index 24ab139..0000000 --- a/modules/parseFileList.mjs +++ /dev/null @@ -1,40 +0,0 @@ -//@ts-check - -import { isNotNull } from "./isNotNull.mjs"; - -/** - * parses a filelist string. That's a string that looks like - * ```md - * name dd-mm-yyyy link name - * name dd-mm-yyyy - * name linkname - * name - * ``` - * - * @param {string} lines - */ -export const parseFileList = (lines) => - // @ts-ignore - lines - .trim() - .split(`\n`) - .map((line, index) => { - const today = new Date().toISOString().split("T")[0]; - const result = line.match( - /(?.+)\.(?\w{2,3})(?:\s+(?[\d-]+)?(?.+))?/ - ); - if (!result) { - console.error(`could not parse line number ${index}`); - return null; - } - const { - // @ts-ignore - groups: { name, ext, date = today, title = name }, - } = result; - const href = `/${name}.${ext}`; - const date_unix = new Date(date).getTime(); - return { name, href, date, title, date_unix, index }; - }) - .filter(isNotNull); - -export default parseFileList; diff --git a/modules/templateBlog.mjs b/modules/templateBlog.mjs index ecfb49d..aa66a1c 100644 --- a/modules/templateBlog.mjs +++ b/modules/templateBlog.mjs @@ -1,4 +1,38 @@ //@ts-check +import { fetchText } from "./utils/fetchText.mjs"; +import { getElementById } from "./utils/getElementById.mjs"; +import { onDocumentKeyUp } from "./utils/onDocumentKey.mjs"; +import { parseFileList } from "./tickle/parseFileList.mjs"; +import {createMenuEntriesFromFileList} from "./tickle/createMenuEntriesFromFileList.mjs" +import { sortFileListLines } from "./tickle/sortFileListLines.mjs" +import { mode } from "./tickle/mode.mjs"; +import {bootstrapRouter} from "./tickle/bootstrapRouter.mjs"; -import blog from "./templateBlogUtils.mjs"; -blog(); +/** + * Loads the article list, parses it, creates the menu items +*/ +export const bootstrap = () => { + + const [Menu, Body, Source, Burger] = ["Menu", "Body", "Source", "Burger"].map( + getElementById + ); + + Burger.addEventListener("click", mode.menuOpen.toggle); + + mode.loading.on(); + fetchText("./files.txt").then((lines)=>{ + const links = parseFileList(lines) + const firstHref = links[0].href; + sortFileListLines(links) + Menu.appendChild(createMenuEntriesFromFileList(links)) + bootstrapRouter(firstHref, (content, raw)=>{ + Body.innerHTML = ""; + Source.innerHTML = raw; + Body.appendChild(content); + }) + }); + + onDocumentKeyUp("?", mode.sourceEnable.toggle); +}; + +export default bootstrap; diff --git a/modules/templateBlogUtils.mjs b/modules/templateBlogUtils.mjs deleted file mode 100644 index b527f4a..0000000 --- a/modules/templateBlogUtils.mjs +++ /dev/null @@ -1,107 +0,0 @@ -//@ts-check -import { changeTitle } from "./changeTitle.mjs"; -import { getCurrentHashUrl } from "./getCurrentHashUrl.mjs"; -import { fetchMarkdown } from "./fetchMarkdown.mjs"; -import { fetchText } from "./fetchText.mjs"; -import { parseFileList } from "./parseFileList.mjs"; -import { documentMode } from "./documentMode.mjs"; -import { getElementById } from "./getElementById.mjs"; -import { onDocumentKeyUp } from "./onDocumentKey.mjs"; - -/***************************************************************** - * - * Creating references to the important stuff - * - ****************************************************************/ - -/** - * The elements we will need - */ -const [Menu, Body, Source, Burger] = ["Menu", "Body", "Source", "Burger"].map( - getElementById -); - -const loadingMode = documentMode("loading"); -const sourceEnableMode = documentMode("source-enabled"); -const menuOpenMode = documentMode("menu-open"); - -/***************************************************************** - * - * Router - * - * Things related to main navigation and to loading pages - * - ****************************************************************/ - -/** - * Listens to hashchange event, and attempts to read the url. - * @param {HashChangeEvent} [_evt] - */ -export const onHashChange = async (_evt) => { - const { path, params } = getCurrentHashUrl(); - if (!path) { - return false; - } - loadingMode.on(); - const { title, raw, content } = await fetchMarkdown(path); - changeTitle(title); - Body.innerHTML = ""; - Source.innerHTML = raw; - Body.appendChild(content); - loadingMode.off(); -}; - -export const loadFileList = () => { - loadingMode.on(); - - fetchText("./files.txt").then(fillMenuFromFileList); -}; - -/** - * Called when the file list is obtained (presumably through loading) - * parses the file list, then fills the side navigation - * If there's no page loaded, it also loads the first page in the list - * (the list gets sorted by date, but the first line is the one that gets used) - * @param {string} lines - */ -export const fillMenuFromFileList = (lines) => { - const links = parseFileList(lines).sort( - ({ date_unix: a }, { date_unix: b }) => a - b - ); - if (links.length < 1) { - return; - } - Menu.innerHTML = links - .map(({ href, title }) => `<a data-link href="#${href}">${title}</a>`) - .join(`\n`); - if (!getCurrentHashUrl().path) { - // @ts-ignore - const href = links.find(({ index }) => index === 0).href; - window.location.hash = `#${href}`; - } else { - onHashChange(); - } -}; - -/***************************************************************** - * - * Bootstrapping - * - * this is where things actually happen - * - ****************************************************************/ - -/** - * Loads the article list, parses it, creates the menu items - */ -export const bootstrap = () => { - Burger.addEventListener("click", menuOpenMode.toggle); - - window.addEventListener("hashchange", onHashChange); - - loadFileList(); - - onDocumentKeyUp("?", sourceEnableMode.toggle); -}; - -export default bootstrap; diff --git a/modules/templateMusings.mjs b/modules/templateMusings.mjs new file mode 100644 index 0000000..aa66a1c --- /dev/null +++ b/modules/templateMusings.mjs @@ -0,0 +1,38 @@ +//@ts-check +import { fetchText } from "./utils/fetchText.mjs"; +import { getElementById } from "./utils/getElementById.mjs"; +import { onDocumentKeyUp } from "./utils/onDocumentKey.mjs"; +import { parseFileList } from "./tickle/parseFileList.mjs"; +import {createMenuEntriesFromFileList} from "./tickle/createMenuEntriesFromFileList.mjs" +import { sortFileListLines } from "./tickle/sortFileListLines.mjs" +import { mode } from "./tickle/mode.mjs"; +import {bootstrapRouter} from "./tickle/bootstrapRouter.mjs"; + +/** + * Loads the article list, parses it, creates the menu items +*/ +export const bootstrap = () => { + + const [Menu, Body, Source, Burger] = ["Menu", "Body", "Source", "Burger"].map( + getElementById + ); + + Burger.addEventListener("click", mode.menuOpen.toggle); + + mode.loading.on(); + fetchText("./files.txt").then((lines)=>{ + const links = parseFileList(lines) + const firstHref = links[0].href; + sortFileListLines(links) + Menu.appendChild(createMenuEntriesFromFileList(links)) + bootstrapRouter(firstHref, (content, raw)=>{ + Body.innerHTML = ""; + Source.innerHTML = raw; + Body.appendChild(content); + }) + }); + + onDocumentKeyUp("?", mode.sourceEnable.toggle); +}; + +export default bootstrap; diff --git a/modules/tickle/bootstrapRouter.mjs b/modules/tickle/bootstrapRouter.mjs new file mode 100644 index 0000000..2ec9d72 --- /dev/null +++ b/modules/tickle/bootstrapRouter.mjs @@ -0,0 +1,41 @@ +//@ts-check + +import { changeTitle } from "../utils/changeTitle.mjs"; +import { getCurrentHashUrl } from "../utils/getCurrentHashUrl.mjs"; +import { fetchMarkdown } from "../utils/fetchMarkdown.mjs"; +import { rewriteLocalUrls } from "../utils/rewriteLocalUrls.mjs"; +import { hasNoHashUrl } from "../utils/getCurrentHashUrl.mjs"; +import { mode } from "./mode.mjs"; + +/** + * Sets up a listener for hashchange events. + * Loads the provided default Href if none is set by the user + * @param {string} defaultHref + * @param {(content: DocumentFragment, raw: string) => void} onPageLoaded + */ +export const bootstrapRouter = async (defaultHref, onPageLoaded) => { + /** + * Listens to hashchange event, and attempts to read the url. + * @param {HashChangeEvent} [_evt] + */ + const onHashChange = async (_evt) => { + const { path } = getCurrentHashUrl(); + if (!path) { + return false; + } + mode.loading.on(); + const { title, raw, content } = await fetchMarkdown(path); + changeTitle(title); + rewriteLocalUrls(content); + onPageLoaded(content, raw) + mode.loading.off(); + }; + + window.addEventListener("hashchange", onHashChange); + + if (hasNoHashUrl()) { + window.location.hash = `#${defaultHref}`; + } else { + onHashChange(); + } +}; diff --git a/modules/tickle/createMenuEntriesFromFileList.mjs b/modules/tickle/createMenuEntriesFromFileList.mjs new file mode 100644 index 0000000..63e3548 --- /dev/null +++ b/modules/tickle/createMenuEntriesFromFileList.mjs @@ -0,0 +1,18 @@ +//@ts-check +import { makeTemplate } from "./utils/print.mjs"; +import { generateDomFromString } from "./utils/generateDomFromString.mjs"; +/** + * @typedef {import("./parseFileList.mjs").ParsedLine} ParsedLine + */ + +const defaultTemplate = `<a data-link href="#{{href}}">{{title}}</a>`; + +/** + * Creates menu entries from a list of links. + * @param {ParsedLine[]} links + * @param {string} template a template for each link, where `{{href}}` is replaced by the href + * and `{{title}}` is replaced by the title. + * all the keys parsed in `parseFileList.mjs` can be used. + */ +export const createMenuEntriesFromFileList = (links, template = defaultTemplate) => + generateDomFromString(links.map(makeTemplate(template)).join("\n")); diff --git a/modules/tickle/index.mjs b/modules/tickle/index.mjs new file mode 100644 index 0000000..d6fa8a1 --- /dev/null +++ b/modules/tickle/index.mjs @@ -0,0 +1,13 @@ +//@ts-check +import { bootstrapRouter } from "./bootstrapRouter.mjs"; +import { createMenuEntriesFromFileList } from "./createMenuEntriesFromFileList.mjs"; +import { parseFileList, parseFileListLine } from "./parseFileList.mjs"; +import { sortFileListLines } from "./sortFileListLines.mjs"; + +export { + bootstrapRouter, + createMenuEntriesFromFileList, + parseFileList, + parseFileListLine, + sortFileListLines, +}; diff --git a/modules/tickle/mode.mjs b/modules/tickle/mode.mjs new file mode 100644 index 0000000..76f5718 --- /dev/null +++ b/modules/tickle/mode.mjs @@ -0,0 +1,8 @@ +//@ts-check +import { documentMode } from "./utils/documentMode.mjs"; + +export const mode = { + loading: documentMode("loading"), + sourceEnable: documentMode("source-enabled"), + menuOpen: documentMode("menu-open"), +}; diff --git a/modules/tickle/parseFileList.mjs b/modules/tickle/parseFileList.mjs new file mode 100644 index 0000000..a691de5 --- /dev/null +++ b/modules/tickle/parseFileList.mjs @@ -0,0 +1,59 @@ +//@ts-check + +import { today } from "./utils/today.mjs"; + +/** + * @typedef {Exclude<ReturnType<typeof parseFileListLine>, null>} ParsedLine + */ + +/** + * Parses a single line of a file list + * @param {string} line + * @param {number} index + * @returns + */ +export const parseFileListLine = (line, index) => { + const result = line.match( + /(?<name>.+)\.(?<ext>\w{2,3})(?:\s+(?<date>[\d-]+)?(?<title>.+))?/ + ); + if (!result) { + return null; + } + const { + //@ts-ignore + groups: { name, ext, date = today, title = name }, + } = result; + const href = `/${name}.${ext}`; + const date_unix = new Date(date).getTime(); + return { name, href, date, title, date_unix, index }; +}; + +/** + * parses a filelist string. That's a string that looks like + * ```md + * name dd-mm-yyyy link name + * name dd-mm-yyyy + * name linkname + * name + * ``` + * This is very specific to Tickle and unlikely to be very useful anywhere else. + * @param {string} lines + */ +export const parseFileList = (lines) => + // @ts-ignore + lines + .trim() + .split(`\n`) + .reduce((/** @type {ParsedLine[]}*/ arr, line, index) => { + const result = parseFileListLine(line.trim(), index); + if (!result) { + console.error(`could not parse line number ${index}`); + return arr; + } + arr.push(result); + return arr; + }, []); + +parseFileList.parseLine = parseFileListLine; + +export default parseFileList; diff --git a/modules/tickle/sortFileListLines.mjs b/modules/tickle/sortFileListLines.mjs new file mode 100644 index 0000000..4d086e7 --- /dev/null +++ b/modules/tickle/sortFileListLines.mjs @@ -0,0 +1,15 @@ +//@ts-check + +/** + * @typedef {import("./parseFileList.mjs").ParsedLine} ParsedLine + */ + +/** + * Sorts a list of parsed lines by a given property in-place + * @param {ParsedLine[]} lines + * @param {keyof ParsedLine} property + */ +export const sortFileListLines = (lines, property = 'date_unix') => + lines.sort(({ [property]: a }, { [property]: b }) => a - b); + +export default sortFileListLines \ No newline at end of file diff --git a/modules/changeTitle.mjs b/modules/utils/changeTitle.mjs similarity index 81% rename from modules/changeTitle.mjs rename to modules/utils/changeTitle.mjs index 6127015..0434918 100644 --- a/modules/changeTitle.mjs +++ b/modules/utils/changeTitle.mjs @@ -7,6 +7,8 @@ * changeTitle.title = "My Site" * changeTitle("Home") // produces "Home | My Site" * ``` + * if not `title` is passed, the document title (as found when running the function + * the first time) will be used. * @param {string} title * @param {string} mainTitle * @returns diff --git a/modules/documentMode.mjs b/modules/utils/documentMode.mjs similarity index 85% rename from modules/documentMode.mjs rename to modules/utils/documentMode.mjs index 3039f46..0cd9342 100644 --- a/modules/documentMode.mjs +++ b/modules/utils/documentMode.mjs @@ -8,6 +8,7 @@ export const documentMode = (name) => ({ on: () => document.body.classList.add(`is-${name}`), off: () => document.body.classList.remove(`is-${name}`), toggle: () => document.body.classList.toggle(`is-${name}`), + has: () => document.body.classList.contains(`is-${name}`), }); export default documentMode; diff --git a/modules/utils/documentState.mjs b/modules/utils/documentState.mjs new file mode 100644 index 0000000..12e0ed1 --- /dev/null +++ b/modules/utils/documentState.mjs @@ -0,0 +1,35 @@ +//@ts-check + +/** + * Creates a document state object that can toggle between exclusive states. + * All passed states' css classnames will be prepended with `mode-`. + * @param {string[]} states + */ +export const documentState = (states) => { + const all = states.map((state) => `mode-${state}`); + + /** + * @param {any} state + * @returns {state is number} + */ + const isValidIndex = (state) => + typeof state === "number" && state >= 0 && state < all.length; + + /** + * @param {number} state + */ + const is = (state) => + isValidIndex(state) && document.body.classList.contains(all[state]); + + /** + * @param {number|undefined|null|false} state + */ + const set = (state = false) => { + document.body.classList.remove(...all); + isValidIndex(state) && document.body.classList.add(all[state]); + }; + + return { set, is }; +}; + +export default documentState; diff --git a/modules/fetchMarkdown.mjs b/modules/utils/fetchMarkdown.mjs similarity index 56% rename from modules/fetchMarkdown.mjs rename to modules/utils/fetchMarkdown.mjs index 0dafc0e..c39ffc1 100644 --- a/modules/fetchMarkdown.mjs +++ b/modules/utils/fetchMarkdown.mjs @@ -2,24 +2,21 @@ import { fetchText } from "./fetchText.mjs"; import { waitIfLocalHost } from "./waitIfLocalHost.mjs"; -import { rewriteLocalUrls } from "./rewriteLocalUrls.mjs"; import { generateDomFromString } from "./generateDomFromString.mjs"; +import {getFirstTitleContent} from "./getFirstTitleContent.mjs"; // @ts-ignore import { micromark } from "https://esm.sh/micromark@3?bundle"; /** - * Loads and parses a markdown document. Makes use of micromark + * Loads and parses a markdown document. Makes use of micromark. * @param {string} path the path to load */ export const fetchMarkdown = (path) => fetchText(path) .then(waitIfLocalHost()) .then((raw) => { - const content = rewriteLocalUrls(generateDomFromString(micromark(raw))); - const firstTitleElement = content.querySelector("h1"); - const title = firstTitleElement - ? firstTitleElement.textContent - : path.replace(/\.\w{2, 4}$/, ""); + const content = generateDomFromString(micromark(raw)); + const title = getFirstTitleContent(content) || path.replace(/\.\w{2, 4}$/, ""); return { title, raw, content }; }); diff --git a/modules/fetchText.mjs b/modules/utils/fetchText.mjs similarity index 100% rename from modules/fetchText.mjs rename to modules/utils/fetchText.mjs diff --git a/modules/utils/generateDomFromString.mjs b/modules/utils/generateDomFromString.mjs new file mode 100644 index 0000000..408f51c --- /dev/null +++ b/modules/utils/generateDomFromString.mjs @@ -0,0 +1,15 @@ +//@ts-check + +/** + * Generates valid dom elements from a string + * @param {string} htmlString + */ +export const generateDomFromString = (htmlString) =>{ + const children = new DOMParser().parseFromString(`<div>${htmlString}</div>`, "text/html") + .children + const fragment = document.createDocumentFragment() + fragment.append(...children) + return fragment + ;} + +export default generateDomFromString; diff --git a/modules/getCurrentHashUrl.mjs b/modules/utils/getCurrentHashUrl.mjs similarity index 64% rename from modules/getCurrentHashUrl.mjs rename to modules/utils/getCurrentHashUrl.mjs index 6551fa9..d9ffe18 100644 --- a/modules/getCurrentHashUrl.mjs +++ b/modules/utils/getCurrentHashUrl.mjs @@ -13,4 +13,11 @@ export const getCurrentHashUrl = () => { return { path, params }; }; +export const hasCurrentHashUrl = () => getCurrentHashUrl().path !== ""; + +export const hasNoHashUrl = () => getCurrentHashUrl().path === ""; + +getCurrentHashUrl.hasCurrentHashUrl = hasCurrentHashUrl; +getCurrentHashUrl.hasNoHashUrl = hasNoHashUrl; + export default getCurrentHashUrl; diff --git a/modules/getElementById.mjs b/modules/utils/getElementById.mjs similarity index 100% rename from modules/getElementById.mjs rename to modules/utils/getElementById.mjs diff --git a/modules/utils/getFirstTitleContent.mjs b/modules/utils/getFirstTitleContent.mjs new file mode 100644 index 0000000..5f08b33 --- /dev/null +++ b/modules/utils/getFirstTitleContent.mjs @@ -0,0 +1,12 @@ +/** + * Returns the first title content in the document, if there is one. + * @param {Node} content + * @returns + */ +export const getFirstTitleContent = (content = document) => { + /** @type {HTMLHeadElement} */ + const firstTitleElement = content.querySelector("h1"); + return firstTitleElement ? firstTitleElement.textContent || "" : ""; +}; + +export default getFirstTitleContent; diff --git a/modules/utils/html.mjs b/modules/utils/html.mjs new file mode 100644 index 0000000..afa6f22 --- /dev/null +++ b/modules/utils/html.mjs @@ -0,0 +1,17 @@ +/** + * Does exactly the same as simply using ``, but allows to + * neatly highlight the html string in editors that support it. + * Packages the resulting string as a template + * @param {TemplateStringsArray} strings + * @param {...any} expressions + */ +export function html(strings, ...expressions){ + const parsed = strings.reduce((previous, current, i) => { + return previous + current + (expressions[i] ? expressions[i] : '') + }, '') + const template = document.createElement("template") + template.innerHTML = parsed + return template +} + +export default html \ No newline at end of file diff --git a/modules/utils/identity.mjs b/modules/utils/identity.mjs new file mode 100644 index 0000000..7dcd8d0 --- /dev/null +++ b/modules/utils/identity.mjs @@ -0,0 +1,17 @@ +/** + * @template T + * @param {T} [value] + * @returns {Promise<T>} + */ +export const awaitedIdentity = (value) => Promise.resolve(value); + +/** + * @template T + * @param {T} [value] + * @returns {T} + */ +export const identity = (value) => value + +identity.awaited = awaitedIdentity; + +export default identity; \ No newline at end of file diff --git a/modules/utils/index.mjs b/modules/utils/index.mjs new file mode 100644 index 0000000..566f4e4 --- /dev/null +++ b/modules/utils/index.mjs @@ -0,0 +1,69 @@ +//@ts-check + +import { changeTitle } from "./changeTitle.mjs"; +import { documentMode } from "./documentMode.mjs"; +import { documentState } from "./documentState.mjs"; +import { fetchMarkdown } from "./fetchMarkdown.mjs"; +import { fetchText } from "./fetchText.mjs"; +import { generateDomFromString } from "./generateDomFromString.mjs"; +import { + getCurrentHashUrl, + hasCurrentHashUrl, + hasNoHashUrl, +} from "./getCurrentHashUrl.mjs"; +import { getElementById } from "./getElementById.mjs"; +import { getFirstTitleContent } from "./getFirstTitleContent.mjs"; +import { identity, awaitedIdentity } from "./identity.mjs"; +import { html } from "./html.mjs"; +import { isExternalUrl } from "./isExternalUrl.mjs"; +import { isLocalHost } from "./isLocalHost.mjs"; +import { isNotNull } from "./isNotNull.mjs"; +import { not } from "./not.mjs"; +import { + onDocumentKeyUp, + onDocumentKeyDown, + onDocumentKey, +} from "./onDocumentKey.mjs"; +import { print, makeTemplate } from "./print.mjs"; +import { + querySelectorDoc, + querySelectorParent, + querySelectorAll, +} from "./querySelectorAll.mjs"; +import { rewriteLocalUrls } from "./rewriteLocalUrls.mjs"; +import { today } from "./today.mjs"; +import { wait } from "./wait.mjs"; +import { waitIfLocalHost } from "./waitIfLocalHost.mjs"; + +export { + changeTitle, + documentMode, + documentState, + fetchMarkdown, + fetchText, + generateDomFromString, + getCurrentHashUrl, + hasCurrentHashUrl, + hasNoHashUrl, + getElementById, + getFirstTitleContent, + html, + identity, + awaitedIdentity, + isExternalUrl, + isLocalHost, + isNotNull, + not, + onDocumentKeyUp, + onDocumentKeyDown, + onDocumentKey, + print, + makeTemplate, + querySelectorDoc, + querySelectorParent, + querySelectorAll, + rewriteLocalUrls, + today, + wait, + waitIfLocalHost, +}; diff --git a/modules/isExternalUrl.mjs b/modules/utils/isExternalUrl.mjs similarity index 100% rename from modules/isExternalUrl.mjs rename to modules/utils/isExternalUrl.mjs diff --git a/modules/isLocalHost.mjs b/modules/utils/isLocalHost.mjs similarity index 100% rename from modules/isLocalHost.mjs rename to modules/utils/isLocalHost.mjs diff --git a/modules/isNotNull.mjs b/modules/utils/isNotNull.mjs similarity index 100% rename from modules/isNotNull.mjs rename to modules/utils/isNotNull.mjs diff --git a/modules/utils/not.mjs b/modules/utils/not.mjs new file mode 100644 index 0000000..b7dcdc1 --- /dev/null +++ b/modules/utils/not.mjs @@ -0,0 +1,9 @@ +/** + * Inverter. + * Easier to read than "!" + * @param {any} a + * @returns + */ +export const not = a => !a + +export default not \ No newline at end of file diff --git a/modules/onDocumentKey.mjs b/modules/utils/onDocumentKey.mjs similarity index 100% rename from modules/onDocumentKey.mjs rename to modules/utils/onDocumentKey.mjs diff --git a/modules/utils/print.mjs b/modules/utils/print.mjs new file mode 100644 index 0000000..8cc8d1f --- /dev/null +++ b/modules/utils/print.mjs @@ -0,0 +1,19 @@ +/** + * Mini-mustache templating system. Simply replaces all occurrences of {{key}} with the value of the key. + * @param {string} str + * @param {Record<string, any>} replacements + */ +export const print = (str, replacements) => + str.replace(/{{(.*?)}}/g, (_match, key) => replacements[key] || '') + + +/** + * + * @param {string} str + * @returns {(replacements: Record<string, any>) => string} + */ +export const makeTemplate = (str) => print.bind(null, str) + +print.makeTemplate = makeTemplate + +export default print \ No newline at end of file diff --git a/modules/querySelectorAll.mjs b/modules/utils/querySelectorAll.mjs similarity index 100% rename from modules/querySelectorAll.mjs rename to modules/utils/querySelectorAll.mjs diff --git a/modules/rewriteLocalUrls.mjs b/modules/utils/rewriteLocalUrls.mjs similarity index 68% rename from modules/rewriteLocalUrls.mjs rename to modules/utils/rewriteLocalUrls.mjs index acf5bbd..e4ad048 100644 --- a/modules/rewriteLocalUrls.mjs +++ b/modules/utils/rewriteLocalUrls.mjs @@ -4,13 +4,13 @@ import isExternalUrl from "./isExternalUrl.mjs"; import { querySelectorParent } from "./querySelectorAll.mjs"; /** - * Makes sure urls to local pages get passed through the routing system - * @param {HTMLElement} container the element containing links to find + * Makes sure urls to local pages are prepended with #/ + * @param {ParentNode} container the element containing links to find */ export const rewriteLocalUrls = (container) => { querySelectorParent(container, "a").forEach((a) => { const href = a.getAttribute("href"); - if (href && !isExternalUrl(href)) { + if (href && !isExternalUrl(href) && !href.startsWith("#")) { a.setAttribute("href", "#/" + href.replace(/^\.?\//, "")); } }); diff --git a/modules/utils/today.mjs b/modules/utils/today.mjs new file mode 100644 index 0000000..313387c --- /dev/null +++ b/modules/utils/today.mjs @@ -0,0 +1,3 @@ +export const today = new Date().toISOString().split("T")[0]; + +export default today \ No newline at end of file diff --git a/modules/wait.mjs b/modules/utils/wait.mjs similarity index 100% rename from modules/wait.mjs rename to modules/utils/wait.mjs diff --git a/modules/waitIfLocalHost.mjs b/modules/utils/waitIfLocalHost.mjs similarity index 76% rename from modules/waitIfLocalHost.mjs rename to modules/utils/waitIfLocalHost.mjs index 831af30..844007a 100644 --- a/modules/waitIfLocalHost.mjs +++ b/modules/utils/waitIfLocalHost.mjs @@ -1,23 +1,16 @@ //@ts-check - import wait from "./wait.mjs"; +import { awaitedIdentity } from "./identity.mjs"; +import isLocalHost from "./isLocalHost.mjs"; -/** - * @template T - * @param {T} [value] - * @returns {Promise<T>} - */ -// @ts-ignore -const identity = (value) => Promise.resolve(value); /** * Waits the specified amount of time before returning the value * @param {number} _durationMs Duration, in milliseconds. Defaults to 1 second * @returns */ -const fakeWait = (_durationMs = 1000) => identity; +const fakeWait = (_durationMs = 1000) => awaitedIdentity; -import isLocalHost from "./isLocalHost.mjs"; /** * useful to check for transitions while developing styles, if the loading screen diff --git a/sally-files.txt b/sally-files.txt new file mode 100644 index 0000000..1789d88 --- /dev/null +++ b/sally-files.txt @@ -0,0 +1,4 @@ +an-armenian-at-a-french-school-in-saudi-arabia.md +thanks-for-the-piano-lessons.md +to-the-cannibals-of-mar-mikhail.md +une-histoire-de-portes-et-de-fenetres.md \ No newline at end of file diff --git a/sally.html b/sally.html new file mode 100644 index 0000000..b29eed5 --- /dev/null +++ b/sally.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <head> + <title>Sally Emerzian + + + + + \ No newline at end of file diff --git a/sally/400016300497_6129063567043520548-1.jpg b/sally/400016300497_6129063567043520548-1.jpg new file mode 100644 index 0000000..b78b6f0 Binary files /dev/null and b/sally/400016300497_6129063567043520548-1.jpg differ diff --git a/sally/an-armenian-at-a-french-school-in-saudi-arabia.md b/sally/an-armenian-at-a-french-school-in-saudi-arabia.md new file mode 100644 index 0000000..91f4a98 --- /dev/null +++ b/sally/an-armenian-at-a-french-school-in-saudi-arabia.md @@ -0,0 +1,28 @@ +--- +date: 2002-07-13T21:30:09Z +title: An Armenian at a French School in Saudi Arabia +author: Sally Émerzian +hero_image: "armenian-french-school" + +--- +Back in the mid 1980’s there was a distinct racial hierarchy at the French school in Saudi Arabia. + +It was most noticeable at lunchtime when the bell rang shrilly through the desert heat, liberating about 700 souls from under the austere stares of their French teachers. + +Like a river from behind a collapsing dam, kids exploded onto the playground in waves of black, brown, and blond, rolling towards the delta of the huge blue tent. Under the long stretches of shade, they formed tight circles of belonging. And invariably, the same outcasts stared at the proud little backs from behind the tent poles. + +There were the Africans from the old French colonies. Probably the most fortunate group in their obliviousness. Their circle was irregular, original, indifferent to the rest of the world. With their fancy braids, and the brightly patterned fabrics of their dresses, they seemed to be perpetually giggling or dancing without as much as a furtive glance wasted on envy or concern for the thoughts of other kids. Their laughter was occasionally accompanied by the sounds of their melodious names as they called each other in their games: Mammadou, Issiakka, Uhmu… They smelled of amber and flashed the blinding whiteness of their carefree smiles on lonely serfs and royalty alike. Genuine and inclusive, they sometimes tugged at the sleeves of random outcasts, inviting them in. But where the circles were not fenced by pigmentation, it was culture that drew the line. And so, the outcasts remained behind the bars of their phantom exile like atheists at a Gospel choir. + +There were the Levantines and the Maghrébins, separate from each other but similar in their implicit slightly more rightful claim on our host country. They were not at home per say, but close enough. In hindsight, they should have represented the group that my skin tone allowed me to infiltrate most easily but their sense of identity was so much crisper than my own and I could never help but feel like an impostor. It would become painfully obvious every year in Arabic class when the teacher would ask "Mirza? Sally Mirza?", and I would answer: "No Madame, A-mirza". + +\-"Amirza, I've never heard that name. Where's your family from?" + +\- "It was Émerzian." I would mumble every year without answering the actual question... + +\- "Ah... Armenian". + +Then there was the royal circle and its annexed courts spread out around it on thrones of multicolored lunchboxes. The French kids watched over their kingdom from behind their golden curls. Once their clear eyes had scanned the ground to make sure they were watched, admired and envied, they proceeded to the display of their belongings: Golden Dolls cold and beautiful, Little Pony’s of plastic with flowing hair and sparkling eyes. They moved them with an air of importance, brushing the nylon with intentional slowness. This was their school, we were all allowed to exist on their grounds thanks to the goodness of their heart. They had little care for anyone, their pretty little heads were always protected. So, patrolling between palace and court, were the guards of failing little boys whose accelerated growth had miraculously slowed their ability to read and write. + +When the bell rang again, all the circles quickly dissolved like separate streams feeding one big canal that reunified everyone under the banner of French education. Lunchtime after lunchtime, year after year, the cycle looped over and over again, like a merry-go-round long since forgotten by an old and senile operator. + +Then one day, silently and without preparation, one of the lonely wooden horses broke free and galloped away to the World Wide Web where I met me and began to live. \ No newline at end of file diff --git a/sally/anastase-maragos-kqkzdnujczq-unsplash.jpg b/sally/anastase-maragos-kqkzdnujczq-unsplash.jpg new file mode 100644 index 0000000..c6b74ac Binary files /dev/null and b/sally/anastase-maragos-kqkzdnujczq-unsplash.jpg differ diff --git a/sally/annie-spratt-fcwmnaarumk-unsplash.jpg b/sally/annie-spratt-fcwmnaarumk-unsplash.jpg new file mode 100644 index 0000000..7498e0e Binary files /dev/null and b/sally/annie-spratt-fcwmnaarumk-unsplash.jpg differ diff --git a/sally/armenian-french-school.png b/sally/armenian-french-school.png new file mode 100644 index 0000000..4ff614d Binary files /dev/null and b/sally/armenian-french-school.png differ diff --git a/sally/artisanmarmikhael.jpg b/sally/artisanmarmikhael.jpg new file mode 100644 index 0000000..6220d7a Binary files /dev/null and b/sally/artisanmarmikhael.jpg differ diff --git a/sally/gregory-morit-gm0a9bae6gu-unsplash.jpg b/sally/gregory-morit-gm0a9bae6gu-unsplash.jpg new file mode 100644 index 0000000..d10c133 Binary files /dev/null and b/sally/gregory-morit-gm0a9bae6gu-unsplash.jpg differ diff --git a/sally/julia-joppien-w-7h8oxrawc-unsplash.jpg b/sally/julia-joppien-w-7h8oxrawc-unsplash.jpg new file mode 100644 index 0000000..3cb3d09 Binary files /dev/null and b/sally/julia-joppien-w-7h8oxrawc-unsplash.jpg differ diff --git a/sally/marmikhael-newproject.jpg b/sally/marmikhael-newproject.jpg new file mode 100644 index 0000000..0001382 Binary files /dev/null and b/sally/marmikhael-newproject.jpg differ diff --git a/sally/marmikhael-village-copy.jpg b/sally/marmikhael-village-copy.jpg new file mode 100644 index 0000000..5660238 Binary files /dev/null and b/sally/marmikhael-village-copy.jpg differ diff --git a/sally/marmikhael-village.jpg b/sally/marmikhael-village.jpg new file mode 100644 index 0000000..ee69e1e Binary files /dev/null and b/sally/marmikhael-village.jpg differ diff --git a/sally/piano.jpg b/sally/piano.jpg new file mode 100644 index 0000000..b56aaf2 Binary files /dev/null and b/sally/piano.jpg differ diff --git a/sally/portrait.jpg b/sally/portrait.jpg new file mode 100644 index 0000000..b99b0ad Binary files /dev/null and b/sally/portrait.jpg differ diff --git a/sally/shitadvertising.jpg b/sally/shitadvertising.jpg new file mode 100644 index 0000000..ff209d6 Binary files /dev/null and b/sally/shitadvertising.jpg differ diff --git a/sally/thanks-for-the-piano-lessons.md b/sally/thanks-for-the-piano-lessons.md new file mode 100644 index 0000000..7b7c1e9 --- /dev/null +++ b/sally/thanks-for-the-piano-lessons.md @@ -0,0 +1,29 @@ +--- +date: 2022-11-07T09:21:21.000+00:00 +hero_image: "piano.jpg" +title: Thank you for the piano lessons +author: Sally Émerzian + +--- +You're downstairs and you can't play, +But you're playing "Strangers In The Night". +For me, it's a school night. +Tomorrow is another day on the front line +Another bloody battle against the world you put me in, then shielded me from. +The awkward notes are far apart +Guesses echoing in the hallway +Against bare walls and marble floors +Like lonely people. +Tonight if I sleep early, tomorrow if I listen carefully, +I might learn all the things you'd have liked to, for you, for me, for me... for you +The burden's heavy on my little heart. +It's counting time, breaking apart +With every new skill you gift me with your time, with my time +Now it's bedtime already but hey, at at least I hear you. +And now I'm your age already but I still see you, +Under that spotlight, behind the piano, below the staircase +The outline of your kind shoulders cast in a mahogany glow +One well-meaning hand figuring out the next note +Til I grow up to be all the things you'd guessed for me. +All those great things that should have made it all worth it +For all the strangers in the night. \ No newline at end of file diff --git a/sally/to-the-cannibals-of-mar-mikhail.md b/sally/to-the-cannibals-of-mar-mikhail.md new file mode 100644 index 0000000..e643ecf --- /dev/null +++ b/sally/to-the-cannibals-of-mar-mikhail.md @@ -0,0 +1,65 @@ +--- +date: 2018-02-01T20:40:38.000+00:00 +title: To the Cannibals of Mar Mikhael +author: Sally Émerzian +hero_image: "marmikhael-village-copy.jpg" +--- +Dear Lebanon, + +"Where is \[your\] mind? Oh where is \[your\] mind..." + +It seems it's time for another love letter... Deep down, you must know they're love letters. + +So, I see that you're boasting a new residential building in Mar Mikhael. Every day, someone pays Facebook to inform me that Bernard Khoury's latest post-apocalyptic structure "Mar Mikhael Village" is the newest addition to Beirut's "most charming" neighborhood. + +For about a million American Dollars, (because the amount in Lebanese pounds would eat up my screen and my brain's ability to quantify), any one of us Almond-Milk-sipping hipsters can own a prestigious flat completely devoid of walls and tiles. + +![](marmikhael-village.jpg) + +All windows. Just endless stretches of glass panes and concrete slabs. The Emperor's New Flat. + +Have you seen this building, Beirut? Is it just me? + +Does it not remind you even a little bit of the ravaged buildings you had to tear down not long ago? The ones hollowed out and partially collapsed by your 25 years of civil war? + +It stands there, a post-modern mangled façade with missing teeth and gaping holes... In a neighborhood that in any other self-respecting nation, would be protected by Urban Planning regulations. + +![](marmikhael-newproject.jpg) + +Do you recall what attracted you to the neighborhood of Mar Mikhael in the first place? It was barely a decade ago that your hip and trendy began to flock to it to become hip and trendy. + +You don't have to look very far. It's in the ad campaign for the development. It's everything the project is not. + +You came to it looking for the last crumbs of your aesthetic identity, the last lines of your national memory. It was Mar Mikhael's characteristic Lebanese architecture that drew you, its old inhabitants, its humble shopkeepers, its quaint residential buildings with ornate little balconies, plain staircases, arched windows and doors... + +![](shitadvertising.jpg) + +How did you fall in love with the charm of Mar Mikhael for what it is, only to slowly try to turn it into downtown Blade Runner? + +How have you become a society built on top of the vestiges of what it once was, both a lover and a destroyer of the faded glory of your golden age? How is it sane to tear down your heritage, to use the space to build something new in which you proceed to hang photos of the old and marvel at how nice it used to be before you tore it down. So you spend hundreds of thousands of dollars celebrating weddings and events in the ruins of your retired train stations while your SUV's herd each other on potholed roads and your sidewalks end before they begin. + +Then you buy into this olive oil, pottery jar, ex-ZaatarWZeit version of yourself and you sell it to the world as your national image, only to progressively turn it into a fast-food joint everywhere it is not owned by the malevolent ghost of Solidere. + +![](artisanmarmikhael.jpg) + +> What has happened to you as a Nation, as a Society, that has led you to become a walking, breathing contradiction, your heart in one place and your choices in another? A Cannibal of your own civilization. + +You find a neighborhood "authentic" and a charming reminder of your roots, you tear it down, you build structures that look nothing like what you like about it, and then you proceed to advertise the thing you built using images of what the neighborhood used to be. I am not sure what the medical word for this condition is. Something between schizophrenia and dementia? + +Tell you what... Let's start over one step at a time. I will begin with myself. Let's restore sanity one brick at a time. + +We can start healing by making peace with walls again. + +Yes walls... Nothing too ambitions. Not talking about big changes, like cleaning up the shores, recycling, not voting for evil baboons. Just simple walls. Let's celebrate walls. Next time a developer tries to sell you a project or a new flat, next time you build a home, expect to see some walls. Just enough to help you locate the windows from amongst other windows. What do you say we make walls trendy again... Think about it, it might even be "avant-garde" and a good business venture. Soon enough, walls will have become so rare that people might begin to miss them. + +After all, our homes were supposed to be regular shelters from regular elements. + +Remember if you can... + +Before parents tucked their children to sleep between the walls of narrow corridors because someone told them it was the safest place to hide from shells. Before street war and shrapnel forced a generation to steer clear of windows. Our walls were just walls and our windows just windows. + +Our souls and our memories cannot possibly subsist on the occasional watercolor painting or boutique hotel repackaging of all that we once called home. + +Yours always for better or worse, + +Sally \ No newline at end of file diff --git a/sally/une-histoire-de-portes-et-de-fenetres.jpg b/sally/une-histoire-de-portes-et-de-fenetres.jpg new file mode 100644 index 0000000..7d0fccb Binary files /dev/null and b/sally/une-histoire-de-portes-et-de-fenetres.jpg differ diff --git a/sally/une-histoire-de-portes-et-de-fenetres.md b/sally/une-histoire-de-portes-et-de-fenetres.md new file mode 100644 index 0000000..eb08e3e --- /dev/null +++ b/sally/une-histoire-de-portes-et-de-fenetres.md @@ -0,0 +1,53 @@ +--- +date: 2020-08-13T23:30:09.000+00:00 +title: Une Histoire de Portes et de Fenêtres +author: Sally Émerzian +hero_image: "anastase-maragos-kqkzdnujczq-unsplash.jpg" + +--- +Lorsque nous étions petits Marc et moi, en Arabie, une de ces fins d’années scolaires, maman nous avait dit que nous allions passer l’été au Liban, mais cette fois, dans la vieille maison de ses parents à Kahaleh. Je devais être en CM2 (7ème) et Marc en CE2 (9ème). + +Pourquoi pas Jounieh? avions-nous demandé, c'est pourtant là bas que se trouvaient ma grand-mère et mes oncles. Qui y avait-il à Kahaleh? + +Personne. Mais aussi tout le monde avant la guerre, nous expliquait-elle: “Vous ne voulez pas voir là où vivait maman?” + +Pour nos yeux d'enfants habitués aux tons sépia du désert de Jeddah, la maison de Kahaleh se dressait comme un domaine enchanté entouré d’un merveilleux jardin secret. Des herbes folles poussaient ici et là, partout entre les marches, à travers les fentes des volets… c’était un royaume de vignes et de vieux rosiers qui pensaient que jamais personne ne reviendrait. + +![](julia-joppien-w-7h8oxrawc-unsplash.jpg) + +Au rez-de-chaussée, il y avait le vieil atelier de menuiserie de mon grand-père Georges. “C’est Geddo lui-même qui a fabriqué toutes les portes, toutes les armoires, et tous les lits” nous racontait-elle. +“Tiens, regardez là-bas, il a même fait un cerf-volant. Tu le veux, Marc?” +“Il est à moi?” demandait-il en écartant les toiles d’araignée. “Est-ce que Geddo savait que je viendrais le prendre?” + +Mais qui savait à l’époque? Lorsque mes grands-parents avaient fermé la porte une première fois, avec quelques affaires jetées vite-fait dans des sacs, c’était pour fuir pour quelques nuits à Bzoummar chez la soeur de mon grand-père. “Juste le temps que les choses se calment un peu”. + +Geddo reviendra encore plusieurs fois réparer des trous dans les portes et les volets. Téta l’accompagnera parfois pour ranger quelques affaires supplémentaires durant des années de déplacement de sous le toit d’une soeur à l’autre. Et parfois, ils rentreront tous pour quelques semaines de paix, jusqu’à ce que les obus remplacent les balles et achèvent de détruire leur attachement aux murs qu’ils aimaient. + +Même couverte de poussière, la maison me semblait neuve. Criblée de trous mais visiblement neuve, dans la mesure où on n’y avait habité que quelques années. Elle contenait encore dans ses placards et ses dressoirs, l’odeur de l’émotion de ses nouveaux habitants. + +Nous suivions maman à travers le long couloir et je me sentais comme Boucle d’Or. +“Ici c’était la chambre de Robert. Là c’est la chambre de Téta et Geddo, et celle-ci la blanche, c’était ma chambre.” + +Elle nous décrivait son bonheur lorsque ses parents avaient achevé la construction. Elle avait finalement sa propre chambre! Avant cela, ils habitaient tous dans la même maison que son oncle et sa tante à Furn-el-Chebbek. Entre frères, soeurs, cousins et cousines, ça faisait 12 enfants sous le même toit. On ne s’ennuyait pas mais c’était quand même une foule! + +Nous passions des jours entiers à essuyer la poussière, des années de poussière. Notre grand ménage ne passait pas inaperçu dans le village. Quelque fois, une vieille voisine frappait à la porte. “Wli hayde ente ka Liliane?? Ma 3reftik!!! Wayyn hal ghaybi?? Kifa Angèle? Allah yer7am bayyik, shou te3ib bhal bayt, khallina sektin…” _(“C’est bien toi Liliane?? Je ne t’ai presque pas reconnue!!! Ça fait si longtemps! Comment va Angèle? Mes condoléances pour ton père... Il a tellement travaillé pour cette maison... laissons tomber…”)._ + +![](annie-spratt-fcwmnaarumk-unsplash.jpg) + +À mon grand bonheur de rat de bibliothèque, je découvrais sur des étagères, les livres laissés par mes oncles et mes tantes. Robert l’anglophile m’a ainsi accidentellement enseigné l’anglais. Sans comprendre grand chose, je rongeais les pages de “Montezuma’s Daughter”, plongée dans un vieux sofa en velours rose capitonné, au fond du salon vestige qui, même à son apogée, n’avait jamais accueilli grand monde. + +Le soir au son des cigales, Marc, enfilant maladroitement un fil de métal dans la moustiquaire, tentait tant bien que mal de confectionner une antenne de fortune pour la grosse caisse de bois de la télévision. Rien n’arrivait à la hauteur de notre excitation lorsqu’on arrivait par hasard à entrevoir, entre les rayures noires et blanches, le visage de l’orpheline Mexicaine Maria Mercedes. + +Pourquoi tous ces souvenirs maintenant, en plein milieu de la nuit à Kiev? + +J’ai 38 ans. J’ai quitté Beyrouth il y a quelques jours. Quatre jours avant la grande explosion. + +J’ai jeté vite-fait quelques affaires dans des sacs, et je suis partie chez mon frère à Kiev. Avant de partir, j’ai donné mes clés à ma tante Yolla. Elle les a ajoutées à un gros trousseau de clés et de promesses faites au fil des ans à tous ceux qui quittaient le pays: que oui bien sûr, “ma te3talo ham, ana w Joseph mendall ntell 3al Beit” _(“Ne vous en faites pas, Joseph et moi on passera souvent jeter un coup d’oeil à la maison”)._ + +C’est Joseph et elle bien-entendu qui ont réparé ma porte et redressé mes fenêtres, pendant que sous le choc, je contemplais le nombre de fois que ma tante et son mari se sont retrouvés en train de balayer le verre des fenêtres éclatées des maisons vides de ceux qu’ils aiment. + +Mais au diable mon appartement à Beyrouth. + +J’ai peur d’avoir des enfants si tard, et d’avoir un jour l’idée de les emmener voir la maison magnifique de mes parents à Baabda, une autre maison abandonnée toute neuve, et de les entendre me suivre avec leurs petits pas et de m’écouter dire: “cette salle de bain, c’est Téta qui l’avait conçue” et “cette balustrade, c’est Geddo qui l’avait fabriquée”, “celle-ci la chambre avec le lit à baldaquins, c’était ma chambre.” + +![](une-histoire-de-portes-et-de-fenetres.jpg) \ No newline at end of file