testing moving the functionality into small modules #5

Open
xananax wants to merge 10 commits from split-into-modules into main
56 changed files with 670 additions and 232 deletions
Showing only changes of commit 7d3f5b67cf - Show all commits

34
components/index.mjs Normal file
View File

@ -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`<p><slot>Hello World!</slot></p>`
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()
}
}
}

View File

@ -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(`<div>${htmlString}</div>`, "text/html")
.firstChild
);
export default generateDomFromString;

View File

@ -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'

View File

@ -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(
/(?<name>.+)\.(?<ext>\w{2,3})(?:\s+(?<date>[\d-]+)?(?<title>.+))?/
);
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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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();
}
};

View File

@ -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"));

13
modules/tickle/index.mjs Normal file
View File

@ -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,
};

8
modules/tickle/mode.mjs Normal file
View File

@ -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"),
};

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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 };
});

View File

@ -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;

View File

@ -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;

View File

@ -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;

17
modules/utils/html.mjs Normal file
View File

@ -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

View File

@ -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;

69
modules/utils/index.mjs Normal file
View File

@ -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,
};

9
modules/utils/not.mjs Normal file
View File

@ -0,0 +1,9 @@
/**
* Inverter.
* Easier to read than "!"
* @param {any} a
* @returns
*/
export const not = a => !a
export default not

19
modules/utils/print.mjs Normal file
View File

@ -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

View File

@ -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(/^\.?\//, ""));
}
});

3
modules/utils/today.mjs Normal file
View File

@ -0,0 +1,3 @@
export const today = new Date().toISOString().split("T")[0];
export default today

View File

@ -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

4
sally-files.txt Normal file
View File

@ -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

9
sally.html Normal file
View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<title>Sally Emerzian</title>
<meta charset="UTF-8" />
<body>
</body>
<script type="module" src="./modules/templateBlog.mjs"></script>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@ -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 1980s 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 Ponys 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1005 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

BIN
sally/artisanmarmikhael.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
sally/piano.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 KiB

BIN
sally/portrait.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

BIN
sally/shitadvertising.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@ -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.

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View File

@ -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 danné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é dun 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. “Cest 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 daraigné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 laccompagnera parfois pour ranger quelques affaires supplémentaires durant des années de déplacement de sous le toit dune soeur à lautre. 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 quils aimaient.
Même couverte de poussière, la maison me semblait neuve. Criblée de trous mais visiblement neuve, dans la mesure où on ny avait habité que quelques années. Elle contenait encore dans ses placards et ses dressoirs, lodeur de lémotion de ses nouveaux habitants.
Nous suivions maman à travers le long couloir et je me sentais comme Boucle dOr.
“Ici cétait la chambre de Robert. Là cest 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 sennuyait 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…” _(“Cest bien toi Liliane?? Je ne tai 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 langlophile ma ainsi accidentellement enseigné langlais. Sans comprendre grand chose, je rongeais les pages de “Montezumas Daughter”, plongée dans un vieux sofa en velours rose capitonné, au fond du salon vestige qui, même à son apogée, navait 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 narrivait à la hauteur de notre excitation lorsquon arrivait par hasard à entrevoir, entre les rayures noires et blanches, le visage de lorpheline Mexicaine Maria Mercedes.
Pourquoi tous ces souvenirs maintenant, en plein milieu de la nuit à Kiev?
Jai 38 ans. Jai quitté Beyrouth il y a quelques jours. Quatre jours avant la grande explosion.
Jai jeté vite-fait quelques affaires dans des sacs, et je suis partie chez mon frère à Kiev. Avant de partir, jai 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 doeil à la maison”)._
Cest 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 quils aiment.
Mais au diable mon appartement à Beyrouth.
Jai peur davoir des enfants si tard, et davoir un jour lidé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, cest Téta qui lavait conçue” et “cette balustrade, cest Geddo qui lavait fabriquée”, “celle-ci la chambre avec le lit à baldaquins, cétait ma chambre.”
![](une-histoire-de-portes-et-de-fenetres.jpg)