Compare commits

..

No commits in common. "8fe48ebd2f9dae321a62664b1e540c8d0ea55759" and "b2d3bdc0a64e63da8850c982acbf606d06822597" have entirely different histories.

19 changed files with 31 additions and 77 deletions

2
.gitattributes vendored
View file

@ -1,3 +1 @@
*.mp4 filter=lfs diff=lfs merge=lfs -text *.mp4 filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 76 KiB

View file

@ -1,11 +0,0 @@
!![una foto de google street view de junio de 2022 de una cocina fantasma en sánchez de bustamante 875 con 8 capturas de pantalla de rappi de distintas marcas operadas en esta cocina](2023-03-09%20Cocinas%20fantasmas%20en%20Rappi%20-%20dataset%20y%20an%C3%A1lisis.md-kitchenita-bustamante.jpg)
Quizás no las conozcas, pero quizás comiste de una _cocina fantasma_. La definición básica de una cocina fantasma es una en la que solo sirve para take-away o delivery, y ya existen hace bastante tiempo. Sin embargo, desde alrededor de ~2017 se generaron otro tipo de cocinas fantasmas: las industriales.
Estas cocinas operan como varias marcas (muchas veces más de 10) en aplicaciones de delivery como Rappi, PedidosYa o Uber Eats. A veces, son un lugar con varias cocinas, o otras veces son una cocina que opera varias marcas. Por más que son un mismo lugar, aparecen como distintas en las aplicaciones de delivery, ofreciendo distintos tipos de comida: sushi, vegana, mexicana, asiática, italiana, china...
Notese que esto es un juego de decepción: generalmente no se aclara que es una cocina fantasma. En algunos casos más extremos en Estados Unidos, estas marcas directamente usan los nombres e identidades de restaurantes locales. [TODO: linkear a esto. le pregunté a doctorow que recuerdo lo había hablado] Sin embargo, por afuera de las aplicaciones, estas compañías se muestran orgullosas (ver [Kitchenita](https://www.kitchenita.co/), [CocinasOcultas](https://cocinasocultas.com/)..)
Por más que (a mi parecer) no se habla mucho de esto en Argentina, es un fenómento que ya existe en Buenos Aires y Córdoba (y probablemente otras ciudades -- no me fijé). Ya las conocía de antes, pero después de ver [este maravilloso video de Eddy Burback](https://www.youtube.com/watch?v=KkIkymh5Ayg) me dió curiosidad investigar localmente que pasa. Y lo hice de la única manera que conozco bien: tecnologicamente :)
Descargué la información de la mayoría de los restaurantes en Rappi en Buenos Aires (aproximadamente 1710, no es exacto por problemas técnicos) y busqué cuales estaban a menos de 15 metros de distancia de entre si. Lo que encontré me sorprendió un poco.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 533 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 B

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 B

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 B

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 B

After

Width:  |  Height:  |  Size: 7.2 KiB

View file

@ -23,8 +23,6 @@ import {
VirtualElement, VirtualElement,
time, time,
article, article,
main,
img,
} from "@nulo/html.js"; } from "@nulo/html.js";
const execFile = promisify(execFileCallback); const execFile = promisify(execFileCallback);
@ -41,6 +39,13 @@ const dateFormatter = new Intl.DateTimeFormat("es-AR", {
const wikilinkExp = /\[\[(.+?)\]\]/giu; const wikilinkExp = /\[\[(.+?)\]\]/giu;
const compilers: {
[key: string]: (config: Config, sourceFileName: string) => Promise<string>;
} = {
".md": compileMarkdownHtml,
".gen": compileExecutableHtml,
};
interface Config { interface Config {
sourcePath: string; sourcePath: string;
buildPath: string; buildPath: string;
@ -94,29 +99,16 @@ async function compilePage(config: Config, sourceFileName: string) {
const title = isIndex ? "nulo.ar" : formatNameToPlainText(name); const title = isIndex ? "nulo.ar" : formatNameToPlainText(name);
const fileConnections = connections.filter(({ linked }) => linked === name); const fileConnections = connections.filter(({ linked }) => linked === name);
let contentHtml, image; const contentHtml = await compileContentHtml(config, sourceFileName);
if (extname(sourceFileName) === ".md") {
({ html: contentHtml, image } = await compileMarkdownHtml(
config,
sourceFileName
));
} else if (extname(sourceFileName) === ".gen")
contentHtml = await compileExecutableHtml(config, sourceFileName);
else throw false;
const html = render( const html = render(
...generateHead(title, name), ...generateHead(title, name),
article( article(
{ itemscope: "", itemtype: "https://schema.org/Article" }, { itemscope: "", itemtype: "https://schema.org/CreativeWork" },
...(isIndex ...(isIndex
? [] ? []
: generateHeader( : generateHeader(name, sourceFileName, fileConnections.length > 0)),
name, raw(contentHtml),
sourceFileName,
fileConnections.length > 0,
image
)),
main({ itemprop: "articleBody" }, raw(contentHtml)),
...generateConnectionsSection(fileConnections) ...generateConnectionsSection(fileConnections)
) )
); );
@ -129,41 +121,25 @@ async function compilePage(config: Config, sourceFileName: string) {
// Get HTML // Get HTML
// ============================================== // ==============================================
type Image = { // TODO: memoize
src: string; function compileContentHtml(
alt: string; config: Config,
}; sourceFileName: string
): Promise<string> {
return compilers[extname(sourceFileName)](config, sourceFileName);
}
async function compileMarkdownHtml( async function compileMarkdownHtml(
config: Config, config: Config,
sourceFileName: string sourceFileName: string
): Promise<{ html: string; image?: Image }> { ): Promise<string> {
let markdown = await readFile( const markdown = await readFile(
join(config.sourcePath, sourceFileName), join(config.sourcePath, sourceFileName),
"utf-8" "utf-8"
); );
const markdownHtml = renderMarkdown(markdown);
let image;
if (markdown.startsWith("!!")) {
const node = reader.parse(markdown.slice(1, markdown.indexOf("\n")));
const imageNode = node.firstChild?.firstChild;
if (!imageNode || !imageNode.destination)
throw new Error("Intenté parsear un ^!! pero no era una imágen");
if (!imageNode.firstChild?.literal)
console.warn(`El ^!! de ${sourceFileName} no tiene alt`);
image = {
src: imageNode.destination,
alt: imageNode.firstChild?.literal || "",
};
markdown = markdown.slice(markdown.indexOf("\n"));
}
const parsed = reader.parse(markdown);
const markdownHtml = writer.render(parsed);
const contentHtml = await hackilyTransformHtml(markdownHtml); const contentHtml = await hackilyTransformHtml(markdownHtml);
return { html: contentHtml, image }; return contentHtml;
} }
async function compileExecutableHtml( async function compileExecutableHtml(
@ -303,14 +279,12 @@ function formatNameToPlainText(name: string): string {
function generateHeader( function generateHeader(
name: string, name: string,
sourceCodePath: string, sourceCodePath: string,
linkConexiones = false, linkConexiones = false
image?: Image
): Renderable[] { ): Renderable[] {
const parsedTitle = parseName(name); const parsedTitle = parseName(name);
return [ return [
a({ href: "." }, "☚ Volver al inicio"), a({ href: "." }, "☚ Volver al inicio"),
header( header(
...(image ? [img({ ...image, itemprop: "image" })] : []),
...("title" in parsedTitle ...("title" in parsedTitle
? [ ? [
h1(parsedTitle.title), h1(parsedTitle.title),
@ -402,6 +376,11 @@ async function scanForConnections(sourcePath: string): Promise<Connection[]> {
// Markdown utils // Markdown utils
// ============================================== // ==============================================
function renderMarkdown(markdown: string) {
const parsed = reader.parse(markdown);
return writer.render(parsed);
}
async function hackilyTransformHtml(html: string): Promise<string> { async function hackilyTransformHtml(html: string): Promise<string> {
html = html html = html
.replaceAll("<a h", '<a rel="noopener noreferrer" h') .replaceAll("<a h", '<a rel="noopener noreferrer" h')
@ -409,8 +388,7 @@ async function hackilyTransformHtml(html: string): Promise<string> {
for (const [match, archivo] of html.matchAll( for (const [match, archivo] of html.matchAll(
/<nulo-sitio-reemplazar-con archivo="(.+?)" \/>/g /<nulo-sitio-reemplazar-con archivo="(.+?)" \/>/g
)) { )) {
if (extname(archivo) !== ".gen") throw false; html = html.replace(match, await compileContentHtml(config, archivo));
html = html.replace(match, await compileExecutableHtml(config, archivo));
} }
return html; return html;
} }

View file

@ -10,23 +10,18 @@ body {
color: #111; color: #111;
color: var(--foreground); color: var(--foreground);
font-family: sans-serif; font-family: sans-serif;
max-width: 75rem; max-width: 45rem;
margin: 0 auto; margin: 0;
padding: 1rem; padding: 1rem;
} }
article header { article header {
margin: 2rem 0; margin: 2rem 0;
text-align: center; text-align: center;
max-width: 75rem;
} }
article h1 { article h1 {
font-size: 32px; font-size: 32px;
} }
article main {
max-width: 45rem;
margin: 0 auto;
}
h2 { h2 {
border-bottom: var(--foreground) solid 1px; border-bottom: var(--foreground) solid 1px;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 57 KiB