Compare commits
No commits in common. "8fe48ebd2f9dae321a62664b1e540c8d0ea55759" and "b2d3bdc0a64e63da8850c982acbf606d06822597" have entirely different histories.
8fe48ebd2f
...
b2d3bdc0a6
2
.gitattributes
vendored
|
@ -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
|
|
||||||
|
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 89 KiB |
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 76 KiB |
|
@ -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.
|
|
BIN
2023-03-09 Cocinas fantasmas en Rappi - dataset y análisis.md-kitchenita-bustamante.jpg
(Stored with Git LFS)
BIN
2023-03-09 Cocinas fantasmas en Rappi - dataset y análisis.md-kitchenita-bustamante.png
(Stored with Git LFS)
Before Width: | Height: | Size: 131 B After Width: | Height: | Size: 533 KiB |
Before Width: | Height: | Size: 131 B After Width: | Height: | Size: 225 KiB |
Before Width: | Height: | Size: 131 B After Width: | Height: | Size: 312 KiB |
Before Width: | Height: | Size: 131 B After Width: | Height: | Size: 184 KiB |
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 129 B After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 129 B After Width: | Height: | Size: 8.8 KiB |
Before Width: | Height: | Size: 129 B After Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 129 B After Width: | Height: | Size: 7.2 KiB |
80
compilar.ts
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
9
drip.css
|
@ -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;
|
||||||
|
|
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 21 KiB |
BIN
se-cayó.jpg
Before Width: | Height: | Size: 130 B After Width: | Height: | Size: 57 KiB |