Compare commits

..

5 commits

Author SHA1 Message Date
8fe48ebd2f 2023-03-09 Cocinas fantasmas en Rappi - dataset y análisis: primer borrador
All checks were successful
repro-run Corre repro-run.json
2023-03-09 21:00:15 -03:00
6180eed52e mover cosas y permitir imagenes GRANDES en el header 2023-03-09 20:59:56 -03:00
68c70d160a la complejidad mata 2023-03-09 20:44:17 -03:00
f64f9ca907 compilar: setear main y hacer schema/Article 2023-03-09 20:40:46 -03:00
9e80b34556 usar lfs para png y jpg..... 2023-03-09 20:22:59 -03:00
19 changed files with 77 additions and 31 deletions

2
.gitattributes vendored
View file

@ -1 +1,3 @@
*.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: 89 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 130 B

View file

@ -0,0 +1,11 @@
!![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: 533 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 312 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 129 B

View file

@ -23,6 +23,8 @@ 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);
@ -39,13 +41,6 @@ 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;
@ -99,16 +94,29 @@ 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);
const contentHtml = await compileContentHtml(config, sourceFileName); 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 html = render( const html = render(
...generateHead(title, name), ...generateHead(title, name),
article( article(
{ itemscope: "", itemtype: "https://schema.org/CreativeWork" }, { itemscope: "", itemtype: "https://schema.org/Article" },
...(isIndex ...(isIndex
? [] ? []
: generateHeader(name, sourceFileName, fileConnections.length > 0)), : generateHeader(
raw(contentHtml), name,
sourceFileName,
fileConnections.length > 0,
image
)),
main({ itemprop: "articleBody" }, raw(contentHtml)),
...generateConnectionsSection(fileConnections) ...generateConnectionsSection(fileConnections)
) )
); );
@ -121,25 +129,41 @@ async function compilePage(config: Config, sourceFileName: string) {
// Get HTML // Get HTML
// ============================================== // ==============================================
// TODO: memoize type Image = {
function compileContentHtml( src: string;
config: Config, alt: string;
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<string> { ): Promise<{ html: string; image?: Image }> {
const markdown = await readFile( let 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 contentHtml; return { html: contentHtml, image };
} }
async function compileExecutableHtml( async function compileExecutableHtml(
@ -279,12 +303,14 @@ 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),
@ -376,11 +402,6 @@ 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')
@ -388,7 +409,8 @@ 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
)) { )) {
html = html.replace(match, await compileContentHtml(config, archivo)); if (extname(archivo) !== ".gen") throw false;
html = html.replace(match, await compileExecutableHtml(config, archivo));
} }
return html; return html;
} }

View file

@ -10,18 +10,23 @@ body {
color: #111; color: #111;
color: var(--foreground); color: var(--foreground);
font-family: sans-serif; font-family: sans-serif;
max-width: 45rem; max-width: 75rem;
margin: 0; margin: 0 auto;
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: 21 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 130 B