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
|
||||
*.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,
|
||||
time,
|
||||
article,
|
||||
main,
|
||||
img,
|
||||
} from "@nulo/html.js";
|
||||
|
||||
const execFile = promisify(execFileCallback);
|
||||
|
@ -41,6 +39,13 @@ const dateFormatter = new Intl.DateTimeFormat("es-AR", {
|
|||
|
||||
const wikilinkExp = /\[\[(.+?)\]\]/giu;
|
||||
|
||||
const compilers: {
|
||||
[key: string]: (config: Config, sourceFileName: string) => Promise<string>;
|
||||
} = {
|
||||
".md": compileMarkdownHtml,
|
||||
".gen": compileExecutableHtml,
|
||||
};
|
||||
|
||||
interface Config {
|
||||
sourcePath: string;
|
||||
buildPath: string;
|
||||
|
@ -94,29 +99,16 @@ async function compilePage(config: Config, sourceFileName: string) {
|
|||
const title = isIndex ? "nulo.ar" : formatNameToPlainText(name);
|
||||
const fileConnections = connections.filter(({ linked }) => linked === name);
|
||||
|
||||
let contentHtml, image;
|
||||
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 contentHtml = await compileContentHtml(config, sourceFileName);
|
||||
|
||||
const html = render(
|
||||
...generateHead(title, name),
|
||||
article(
|
||||
{ itemscope: "", itemtype: "https://schema.org/Article" },
|
||||
{ itemscope: "", itemtype: "https://schema.org/CreativeWork" },
|
||||
...(isIndex
|
||||
? []
|
||||
: generateHeader(
|
||||
name,
|
||||
sourceFileName,
|
||||
fileConnections.length > 0,
|
||||
image
|
||||
)),
|
||||
main({ itemprop: "articleBody" }, raw(contentHtml)),
|
||||
: generateHeader(name, sourceFileName, fileConnections.length > 0)),
|
||||
raw(contentHtml),
|
||||
...generateConnectionsSection(fileConnections)
|
||||
)
|
||||
);
|
||||
|
@ -129,41 +121,25 @@ async function compilePage(config: Config, sourceFileName: string) {
|
|||
// Get HTML
|
||||
// ==============================================
|
||||
|
||||
type Image = {
|
||||
src: string;
|
||||
alt: string;
|
||||
};
|
||||
// TODO: memoize
|
||||
function compileContentHtml(
|
||||
config: Config,
|
||||
sourceFileName: string
|
||||
): Promise<string> {
|
||||
return compilers[extname(sourceFileName)](config, sourceFileName);
|
||||
}
|
||||
|
||||
async function compileMarkdownHtml(
|
||||
config: Config,
|
||||
sourceFileName: string
|
||||
): Promise<{ html: string; image?: Image }> {
|
||||
let markdown = await readFile(
|
||||
): Promise<string> {
|
||||
const markdown = await readFile(
|
||||
join(config.sourcePath, sourceFileName),
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
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 markdownHtml = renderMarkdown(markdown);
|
||||
const contentHtml = await hackilyTransformHtml(markdownHtml);
|
||||
return { html: contentHtml, image };
|
||||
return contentHtml;
|
||||
}
|
||||
|
||||
async function compileExecutableHtml(
|
||||
|
@ -303,14 +279,12 @@ function formatNameToPlainText(name: string): string {
|
|||
function generateHeader(
|
||||
name: string,
|
||||
sourceCodePath: string,
|
||||
linkConexiones = false,
|
||||
image?: Image
|
||||
linkConexiones = false
|
||||
): Renderable[] {
|
||||
const parsedTitle = parseName(name);
|
||||
return [
|
||||
a({ href: "." }, "☚ Volver al inicio"),
|
||||
header(
|
||||
...(image ? [img({ ...image, itemprop: "image" })] : []),
|
||||
...("title" in parsedTitle
|
||||
? [
|
||||
h1(parsedTitle.title),
|
||||
|
@ -402,6 +376,11 @@ async function scanForConnections(sourcePath: string): Promise<Connection[]> {
|
|||
// Markdown utils
|
||||
// ==============================================
|
||||
|
||||
function renderMarkdown(markdown: string) {
|
||||
const parsed = reader.parse(markdown);
|
||||
return writer.render(parsed);
|
||||
}
|
||||
|
||||
async function hackilyTransformHtml(html: string): Promise<string> {
|
||||
html = html
|
||||
.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(
|
||||
/<nulo-sitio-reemplazar-con archivo="(.+?)" \/>/g
|
||||
)) {
|
||||
if (extname(archivo) !== ".gen") throw false;
|
||||
html = html.replace(match, await compileExecutableHtml(config, archivo));
|
||||
html = html.replace(match, await compileContentHtml(config, archivo));
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
|
9
drip.css
|
@ -10,23 +10,18 @@ body {
|
|||
color: #111;
|
||||
color: var(--foreground);
|
||||
font-family: sans-serif;
|
||||
max-width: 75rem;
|
||||
margin: 0 auto;
|
||||
max-width: 45rem;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
article header {
|
||||
margin: 2rem 0;
|
||||
text-align: center;
|
||||
max-width: 75rem;
|
||||
}
|
||||
article h1 {
|
||||
font-size: 32px;
|
||||
}
|
||||
article main {
|
||||
max-width: 45rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h2 {
|
||||
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 |