diff --git a/00_pendahuluan.md b/00_pendahuluan.md new file mode 100644 index 000000000..c6c595488 --- /dev/null +++ b/00_pendahuluan.md @@ -0,0 +1,271 @@ +{{meta {load_files: ["code/intro.js"]}}} + +# Pendahuluan + +{{quote {author: "Ellen Ullman", title: "Close to the Machine: Technophilia and Its Discontents", chapter: true} + +Kita berpikir bahwa kita membuat sistem tersebut untuk tujuan kita sendiri. Kita berpikir kita sedang membuatnya dari cerminan diri kita sendiri... Tetapi komputer sebenarnya tidak seperti kita sama sekali. Komputer adalah proyeksi dari bagian kecil saja dari diri kita: yaitu bagian yang berdedikasi terhadap logika, urutan, aturan, dan kejelasan. + +quote}} + +{{figure {url: "img/chapter_picture_00.jpg", alt: "Illustration of a screwdriver next to a circuit board of about the same size", chapter: "framed"}}} + +Buku ini adalah mengenai cara memerintah ((komputer)). Saat ini, komputer ada dimana-mana seperti halnya sebuah obeng, tapi mereka memang lebih rumit, dan untuk membuatnya melakukan sesuatu seperti yang kita mau tidaklah selalu mudah. + +Jika pekerjaan yang anda berikan kepada komputer adalah pekerjaan yang umum, sangat dipahami orang, seperti halnya menunjukkan surel anda atau beraksi layaknya sebuah kalkulator, anda bisa saja membuka ((aplikasi)) yang cocok untuk pekerjaan itu dan anda bisa langsung bekerja. Tetapi untuk pekerjaan-pekerjaan yang unik atau terbuka akhirnya, seringkali tidak ada aplikasi yang cocok. + +Di sini lah ((pemrograman)) mungkin bisa hadir. _Pemrograman_ adalah sebuah pekerjaan untuk membangun sebuah _program)_—sebuah kumpulan instruksi-instruksi presisi yang memberitahu komputer apa yang harus ia lakukan. Karena komputer itu monster yang bodoh dan pedantik, pemrograman pada dasarnya sebuah aktivitas yang melelahkan dan membuat frustasi. + +Untungnya, jika anda bisa mengabaikan fakta tersebut dan bahkan anda bisa menikmati berpikir keras seperti halnya sebuah mesin yang bodoh, maka pemrograman bisa menjadi menyenangkan. Ia memungkinkan anda melakukan sesuatu yang biasanya mengabiskan waktu berjam-jam menjadi selesai hanya dalam hitungan detik. Ia juga cara untuk membuat komputer bisa melakukan hal-hal yang sebelumnya tak bisa mereka lakukan. Selain itu, ia juga bisa menjadi sebuah permainan yang penuh dengan teka-teki dan abstraksi.\*\*\*\* + +Kebanyakan pemrograman dilakukan dengan ((bahasa pemrograman)). Sebuah _bahasa pemrograman_ adalah sebuah bahasa yang sengaja dibuat untuk memerintahkan komputer melakukan sesuatu. Hal paling menarik dari hal ini adalah kita menemukan bahwa cara untuk berkomunikasi dengan komputer adalah terinspirasi dari cara kita berkomunikasi dengan sesama manusia. Seperti halnya bahasa untuk manusia, bahasa pemrograman memiliki kata-kata dan frasa-frasa yang digabungkan dalam kesatuan yang baru, sehingga membuatnya bisa menjelaskan konsep-konsep yang baru. + +Di masa lalu, antar-muka berbasis bahasa seperti BASIC dan DOS di tahun 1980-an dan 1990-an, adalah metode utama untuk berinteraksi dengan komputer. Untuk penggunaan rutin, antar-muka ini telah digantikan oleh antar-muka visual yang lebih mudah dipelajari tapi kurang memberikan kebebasan. Jika anda tahu caranya, bahasa-bahasa tersebut masih tersedia. Salah satu diantaranya, _JavaScript_, disertakan bersama setiap peramban mutakhir dan tersedia di hampir semua perangkat. + +Buku ini akan mencoba untuk mengakrabkan anda dengan bahasa ini sehingga anda bisa melakukan hal-hal yang berguna dan menyenangkan dengannya. + +## Mengenai Pemrograman + +{{index [programming, "difficulty of"]}} + +Selain menjelaskan JavaScript, Saya akan memperkenalkan prinsip-prinsip dasar pemrograman. Pemrograman itu ternyata sulit. Aturan-aturan dasarnya sebenarnya sederhana dan jelas, tetapi program-program yang dibuat di atas aturan-aturan tersebut cenderung menjadi cukup kompleks sehingga memunculkan aturan-aturan dan kompleksitasnya tersendiri. Anda sebenarnya sedang membangun labirin anda sendiri dan anda bisa dengan mudah tersesat di dalamnya. + +{{index learning}} + +Akan ada saatnya ketika anda membaca buku ini dan merasa frustasi. Jika ada masih pemula dalam pemrograman, anda akan menemui banyak sekali materi baru untuk dicerna. Banyak dari materi ini yang akan digabungkan dengan cara tertentu yang meminta anda untuk membuat hubungan-hubungan baru. + +Ini sangat tergantung pada apakah anda mau melakukan usaha yang diperlukan. Ketika anda kesulitan mengikuti buku ini, jangan langsung mengambil kesimpulan mengenai kemampuan anda sendiri. Anda sudah keren - tinggal teruskan saja usahanya. Istirahatlah, baca ulang sebagian materinya, dan pastikan bahwa anda membaca dan mengerti program contoh dan ((latihan)). Belajar butuh kerja keras, namun semua hal yang anda pelajari akan menjadi milik anda sendiri dan akan membuat proses belajar anda di kemudian hari lebih mudah. + +{{quote {author: "Ursula K. Le Guin", title: "The Left Hand of Darkness"} + +{{index "Le Guin, Ursula K."}} + +Ketika aksi tumbuh tanpa untung, kumpulkan informasi; ketika informasi tumbuh tanpa untung, tidurlah. + +quote}} + +{{index [program, "nature of"], data}} + +Sebuah program bisa berupa banyak hal. Ia adalah sebuah tulisan karya seorang _programmer_, yang mengarahkan sebuah komputer agar melakukan tugasnya, sebuah data di ingatan komputer, dan pada saat yang bersamaan, mengendalikan aksi yang dilakukan pada ingatan ini. Analogi-analogi yang mencoba membandingkan program dengan barang-barang sehari-hari lain biasanya tidak cocok. Sebuah analogi yang agak cocok secara permukaan adalah yang membandingkan program dengan sebuah mesin dimana terdapat banyak bagian dan untuk membuat mesin tersebut berjalan, kita harus mempertimbangkan bagaimana bagian-bagian tersebut saling berhubungan dan bagaimana mereka bisa berkontribusi kepada seluruh operasinya. + +Sebuah komputer adalah mesin fisik yang bertindak sebagai tuan rumah bagi mesin-mesin abstrak ini. Komputer itu sendiri bodoh, dia hanya bisa melakukan hal-hal yang didefinisikan dengan jelas. Komputer itu bermanfaat hanya karena mereka bisa melakukan hal-hal tersebut dengan amat sangat cepat. Sebuah program bisa dengan cerdas menggabungkan aksi-aksi sederhana tersebut untuk melakukan hal-hal yang yang rumit. + +{{index [programming, "joy of"]}} + +Sebuah program adalah bangunan pemikiran. Murah untuk dibangun, tak memiliki berat, dan bisa tumbuh dengan mudah dengan ketikan tangan kita. Tetapi, seiring dengan tumbuhnya program kita, begitu pula kompleksitasnya. Keahlian pemrograman adalah keahlian membuat program yang tidak membuat pemrogram lainnya kebingungan dengan program itu. Program yang terbaik adalah program yang melakukan hal yang menarik sambil tetap mudah dipahami. + +{{index "programming style", "best practices"}} + +Sebagian pemrogram meyakini bahwa kerumitan tersebut bisa ditangani dengan menggunakan beberapa teknik-teknik yang mereka pahami. Mereka memiliki aturan baku ("praktek terbaik") yang menjadi resep untuk membangun program dengan benar, dan mereka setia di dalam zona aman yang kecil itu. + +{{index experiment}} + +Ini sebenarnya bukan hanya membosankan, tapi juga tidak efektif. Masalah-masalah baru seringkali membutuhkan penyelesaian-penyelesaian baru. Disiplin ilmu pemrograman masih muda dan terus berkembang dengan cepat. Bidang ilmunya cukup bervariasi sehingga membuka ruang untuk pendekatan-pendekatan sangat berbeda satu sama lain. Ada banyak sekali kesalahan yang buruk dalam rancangan sebuah program, sehingga anda seharusnya melakukan kesalahan tersebut paling tidak sekali untuk memahaminya. Naluri untuk memahami bagaimana bentuk sebuah program yang bagus itu bisa terbentuk dengan latihan, bukan dengan belajar dari daftar aturan. + +## Mengapa bahasa itu penting + +{{index "programming language", "machine code", "binary data"}} + +Pada awal kelahiran komputer, belum ada bahasa pemrograman. Program saat itu berbentuk seperti ini: + +```{lang: null} +00110001 00000000 00000000 +00110001 00000001 00000001 +00110011 00000001 00000010 +01010001 00001011 00000010 +00100010 00000010 00001000 +01000011 00000001 00000000 +01000001 00000001 00000001 +00010000 00000010 00000000 +01100010 00000000 00000000 +``` + +{{index [programming, "history of"], "punch card", complexity}} + +Ini adalah program untuk menambahkan bilangan dari 1 sampai 10 dan mencetak hasilnya: `1 + 2 + ... + 10 = 55`. Program ini bisa berjalan pada sebuah mesin hipotetis. Untuk memprogram komputer di masa-masa awal, anda harus menyediakan sekumpulan besar saklar dengan urutan yang benar atau menyiapkan lubang pukul (_punch hole_) dalam selembar papan untuk dimasukkan ke dalam komputer. Anda bisa membayangkan betapa repot dan rawan kesalahan prosedur ini. Untuk menulis sebuah program sederhana saja, harus dengan kepandaian dan disiplin yang tinggi. Apatah lagi jika programnya kompleks. + +{{index bit, "wizard (mighty)"}} + +Tentu saja, memasukkan pola-pola bit kuno ini secara manual membuat pemrogram merasa layaknya penyihir. Hal ini sedikit banyak berkontribusi terhadap kepuasan bekerja. + +{{index memory, instruction}} + +Tiap baris dari program di atas mengandung instruksi tunggal. Ia dapat ditulis dalam bahasa Indonesia seperti ini: + +1. Simpan angka 0 di lokasi memori 0. +2. Simpan angka 1 di lokasi memori 1. +3. Simpan nilai dari lokasi memori 1 di lokasi memori 2. +4. Kurangi angka 11 dari nilai lokasi memori 2. +5. Jika nilai lokasi memori 2 adalah 0, lanjutkan dengan instruksi 9. +6. Tambahkan nilai lokasi memori 1 ke lokasi memori 0. +7. Tambahkan angka 1 ke nilai lokasi memori 1. +8. Lanjutkan dengan instruksi 3. +9. Keluarkan nilai lokasi memori 0. + +{{index readability, naming, binding}} + +Although that is already more readable than the soup of bits, it is still rather obscure. Using names instead of numbers for the instructions and memory locations helps. + +```{lang: "null"} + Set “total” to 0. + Set “count” to 1. +[loop] + Set “compare” to “count”. + Subtract 11 from “compare”. + If “compare” is 0, continue at [end]. + Add “count” to “total”. + Add 1 to “count”. + Continue at [loop]. +[end] + Output “total”. +``` + +{{index loop, jump, "summing example"}} + +Can you see how the program works at this point? The first two lines give two memory locations their starting values: `total` will be used to build up the result of the computation, and `count` will keep track of the number that we are currently looking at. The lines using `compare` are probably the most confusing ones. The program wants to see whether `count` is equal to 11 to decide whether it can stop running. Because our hypothetical machine is rather primitive, it can test only whether a number is zero and make a decision based on that. It therefore uses the memory location labeled `compare` to compute the value of `count - 11` and makes a decision based on that value. The next two lines add the value of `count` to the result and increment `count` by 1 every time the program decides that `count` is not 11 yet. + +Here is the same program in JavaScript: + +``` +let total = 0, count = 1; +while (count <= 10) { + total += count; + count += 1; +} +console.log(total); +// → 55 +``` + +{{index "while loop", loop, [braces, block]}} + +This version gives us a few more improvements. Most importantly, there is no need to specify the way we want the program to jump back and forth anymore—the `while` construct takes care of that. It continues executing the block (wrapped in braces) below it as long as the condition it was given holds. That condition is `count <= 10`, which means “the count is less than or equal to 10”. We no longer have to create a temporary value and compare that to zero, which was just an uninteresting detail. Part of the power of programming languages is that they can take care of uninteresting details for us. + +{{index "console.log"}} + +At the end of the program, after the `while` construct has finished, the `console.log` operation is used to write out the result. + +{{index "sum function", "range function", abstraction, function}} + +Finally, here is what the program could look like if we happened to have the convenient operations `range` and `sum` available, which respectively create a ((collection)) of numbers within a range and compute the sum of a collection of numbers: + +```{startCode: true} +console.log(sum(range(1, 10))); +// → 55 +``` + +{{index readability}} + +The moral of this story is that the same program can be expressed in both long and short, unreadable and readable ways. The first version of the program was extremely obscure, whereas this last one is almost English: `log` the `sum` of the `range` of numbers from 1 to 10. (We will see in [later chapters](data) how to define operations like `sum` and `range`.) + +{{index ["programming language", "power of"], composability}} + +A good programming language helps the programmer by allowing them to talk about the actions that the computer has to perform on a higher level. It helps omit details, provides convenient building blocks (such as `while` and `console.log`), allows you to define your own building blocks (such as `sum` and `range`), and makes those blocks easy to compose. + +## What is JavaScript? + +{{index history, Netscape, browser, "web application", JavaScript, [JavaScript, "history of"], "World Wide Web"}} + +{{indexsee WWW, "World Wide Web"}} + +{{indexsee Web, "World Wide Web"}} + +JavaScript was introduced in 1995 as a way to add programs to web pages in the Netscape Navigator browser. The language has since been adopted by all other major graphical web browsers. It has made modern web applications possible—that is, applications with which you can interact directly without doing a page reload for every action. JavaScript is also used in more traditional websites to provide various forms of interactivity and cleverness. + +{{index Java, naming}} + +It is important to note that JavaScript has almost nothing to do with the programming language named Java. The similar name was inspired by marketing considerations rather than good judgment. When JavaScript was being introduced, the Java language was being heavily marketed and was gaining popularity. Someone thought it was a good idea to try to ride along on this success. Now we are stuck with the name. + +{{index ECMAScript, compatibility}} + +After its adoption outside of Netscape, a ((standard)) document was written to describe the way the JavaScript language should work so that the various pieces of software that claimed to support JavaScript could make sure they actually provided the same language. This is called the ECMAScript standard, after the Ecma International organization that conducted the standardization. In practice, the terms ECMAScript and JavaScript can be used interchangeably—they are two names for the same language. + +{{index [JavaScript, "weaknesses of"], debugging}} + +There are those who will say _terrible_ things about JavaScript. Many of these things are true. When I was required to write something in JavaScript for the first time, I quickly came to despise it. It would accept almost anything I typed but interpret it in a way that was completely different from what I meant. This had a lot to do with the fact that I did not have a clue what I was doing, of course, but there is a real issue here: JavaScript is ridiculously liberal in what it allows. The idea behind this design was that it would make programming in JavaScript easier for beginners. In actuality, it mostly makes finding problems in your programs harder because the system will not point them out to you. + +{{index [JavaScript, "flexibility of"], flexibility}} + +This flexibility also has its advantages, though. It leaves room for techniques that are impossible in more rigid languages and makes for a pleasant, informal style of programming. After ((learning)) the language properly and working with it for a while, I have come to actually _like_ JavaScript. + +{{index future, [JavaScript, "versions of"], ECMAScript, "ECMAScript 6"}} + +There have been several versions of JavaScript. ECMAScript version 3 was the widely supported version during JavaScript's ascent to dominance, roughly between 2000 and 2010. During this time, work was underway on an ambitious version 4, which planned a number of radical improvements and extensions to the language. Changing a living, widely used language in such a radical way turned out to be politically difficult, and work on version 4 was abandoned in 2008. A much less ambitious version 5, which made only some uncontroversial improvements, came out in 2009. In 2015, version 6 came out, a major update that included some of the ideas planned for version 4. Since then we've had new, small updates every year. + +The fact that JavaScript is evolving means that browsers have to constantly keep up. If you're using an older browser, it may not support every feature. The language designers are careful to not make any changes that could break existing programs, so new browsers can still run old programs. In this book, I'm using the 2024 version of JavaScript. + +{{index [JavaScript, "uses of"]}} + +Web browsers are not the only platforms on which JavaScript is used. Some databases, such as MongoDB and CouchDB, use JavaScript as their scripting and query language. Several platforms for desktop and server programming, most notably the ((Node.js)) project (the subject of [Chapter ?](node)), provide an environment for programming JavaScript outside of the browser. + +## Code, and what to do with it + +{{index "reading code", "writing code"}} + +_Code_ is the text that makes up programs. Most chapters in this book contain quite a lot of code. I believe reading code and writing ((code)) are indispensable parts of ((learning)) to program. Try to not just glance over the examples—read them attentively and understand them. This may be slow and confusing at first, but I promise that you'll quickly get the hang of it. The same goes for the ((exercises)). Don't assume you understand them until you've actually written a working solution. + +{{index interpretation}} + +I recommend you try your solutions to exercises in an actual JavaScript interpreter. That way, you'll get immediate feedback on whether what you are doing is working, and, I hope, you'll be tempted to ((experiment)) and go beyond the exercises. + +{{if interactive + +When reading this book in your browser, you can edit (and run) all example programs by clicking them. + +if}} + +{{if book + +{{index download, sandbox, "running code"}} + +The easiest way to run the example code in the book—and to experiment with it—is to look it up in the online version of the book at [_https://eloquentjavascript.net_](https://eloquentjavascript.net/). There, you can click any code example to edit and run it and to see the output it produces. To work on the exercises, go to [_https://eloquentjavascript.net/code_](https://eloquentjavascript.net/code), which provides starting code for each coding exercise and allows you to look at the solutions. + +if}} + +{{index "developer tools", "JavaScript console"}} + +Running the programs defined in this book outside of the book's website requires some care. Many examples stand on their own and should work in any JavaScript environment. But code in later chapters is often written for a specific environment (the browser or Node.js) and can run only there. In addition, many chapters define bigger programs, and the pieces of code that appear in them depend on each other or on external files. The [sandbox](https://eloquentjavascript.net/code) on the website provides links to ZIP files containing all the scripts and data files necessary to run the code for a given chapter. + +## Overview of this book + +This book contains roughly three parts. The first 12 chapters discuss the JavaScript language. The next seven chapters are about web ((browsers)) and the way JavaScript is used to program them. Finally, two chapters are devoted to ((Node.js)), another environment to program JavaScript in. There are five _project chapters_ in the book that describe larger example programs to give you a taste of actual programming. + +The language part of the book starts with four chapters that introduce the basic structure of the JavaScript language. They discuss [control structures](program_structure) (such as the `while` word you saw in this introduction), [functions](functions) (writing your own building blocks), and [data structures](data). After these, you will be able to write basic programs. Next, Chapters [?](higher_order) and [?](object) introduce techniques to use functions and objects to write more _abstract_ code and keep complexity under control. + +After a [first project chapter](robot) that builds a crude delivery robot, the language part of the book continues with chapters on [error handling and bug fixing](error), [regular expressions](regexp) (an important tool for working with text), [modularity](modules) (another defense against complexity), and [asynchronous programming](async) (dealing with events that take time). The [second project chapter](language), where we implement a programming language, concludes the first part of the book. + +The second part of the book, Chapters [?](browser) to [?](paint), describes the tools that browser JavaScript has access to. You'll learn to display things on the screen (Chapters [?](dom) and [?](canvas)), respond to user input ([Chapter ?](event)), and communicate over the network ([Chapter ?](http)). There are again two project chapters in this part: building a [platform game](game) and a [pixel paint program](paint). + +[Chapter ?](node) describes Node.js, and [Chapter ?](skillsharing) builds a small website using that tool. + +{{if commercial + +Finally, [Chapter ?](fast) describes some of the considerations that come up when optimizing JavaScript programs for speed. + +if}} + +## Typographic conventions + +{{index "factorial function"}} + +In this book, text written in a `monospaced` font will represent elements of programs. Sometimes these are self-sufficient fragments, and sometimes they just refer to part of a nearby program. Programs (of which you have already seen a few) are written as follows: + +``` +function factorial(n) { + if (n == 0) { + return 1; + } else { + return factorial(n - 1) * n; + } +} +``` + +{{index "console.log"}} + +Sometimes, to show the output that a program produces, the expected output is written after it, with two slashes and an arrow in front. + +``` +console.log(factorial(8)); +// → 40320 +``` + +Good luck! diff --git a/Makefile b/Makefile index c4ea65417..d4aaad577 100644 --- a/Makefile +++ b/Makefile @@ -40,16 +40,14 @@ test: html tex: $(foreach CHAP,$(CHAPTERS),pdf/$(CHAP).tex) pdf/hints.tex $(patsubst img/%.svg,img/generated/%.pdf,$(SVGS)) -book.pdf: tex pdf/book.tex - cd pdf && sh build.sh book > /dev/null - mv pdf/book.pdf . +book.pdf: tex pdf/book.tex src/build_book_pdf.mjs + node src/build_book_pdf.mjs book book.pdf -pdf/book_mobile.tex: pdf/book.tex - cat pdf/book.tex | sed -e 's/natbib}/natbib}\n\\usepackage[a5paper, left=5mm, right=5mm]{geometry}/' | sed -e 's/setmonofont.Scale=0.8./setmonofont[Scale=0.75]/' > pdf/book_mobile.tex +pdf/book_mobile.tex: pdf/book.tex src/build_mobile_tex.mjs + node src/build_mobile_tex.mjs pdf/book.tex pdf/book_mobile.tex -book_mobile.pdf: pdf/book_mobile.tex tex - cd pdf && sh build.sh book_mobile > /dev/null - mv pdf/book_mobile.pdf . +book_mobile.pdf: pdf/book_mobile.tex tex src/build_book_pdf.mjs + node src/build_book_pdf.mjs book_mobile book_mobile.pdf pdf/hints.tex: $(foreach CHAP,$(CHAPTERS),$(CHAP).md) src/extract_hints.mjs node src/extract_hints.mjs | node src/render_latex.mjs - > $@ diff --git a/NodeMakefile b/NodeMakefile new file mode 100644 index 000000000..b3d464348 --- /dev/null +++ b/NodeMakefile @@ -0,0 +1,83 @@ +CHAPTERS := $(basename $(shell ls [0-9][0-9]_*.md) .md) + +SVGS := $(wildcard img/*.svg) + +all: html book.pdf book_mobile.pdf book.epub book.mobi + +html: $(foreach CHAP,$(CHAPTERS),html/$(CHAP).html) html/ejs.js \ + code/skillsharing.zip code/solutions/20_3_a_public_space_on_the_web.zip html/code/chapter_info.js + +html/%.html: %.md src/render_html.mjs src/chapter.html + node src/render_html.mjs $< > $@ + node src/build_code.mjs $< + +html/code/chapter_info.js: $(foreach CHAP,$(CHAPTERS),$(CHAP).md) code/solutions/* src/chapter_info.mjs + node src/chapter_info.mjs > html/code/chapter_info.js + +html/ejs.js: node_modules/codemirror/dist/index.js \ + node_modules/@codemirror/view/dist/index.js \ + node_modules/@codemirror/state/dist/index.js \ + node_modules/@codemirror/language/dist/index.js \ + node_modules/@codemirror/lang-html/dist/index.js \ + node_modules/@codemirror/lang-javascript/dist/index.js \ + node_modules/acorn/dist/acorn.js \ + node_modules/acorn-walk/dist/walk.js \ + src/client/*.mjs + node_modules/.bin/rollup -c src/client/rollup.config.mjs + +code/skillsharing.zip: html/21_skillsharing.html code/skillsharing/package.json + rm -f $@ + cd code; node ../src/make_zip.mjs skillsharing.zip skillsharing/*.mjs skillsharing/package.json skillsharing/public/*.* + +code/solutions/20_3_a_public_space_on_the_web.zip: $(wildcard code/solutions/20_3_a_public_space_on_the_web/*) + rm -f $@ + cd code/solutions; node ../../src/make_zip.mjs 20_3_a_public_space_on_the_web.zip 20_3_a_public_space_on_the_web/* + +test: html + @for F in $(CHAPTERS); do echo Testing $$F:; node src/run_tests.mjs $$F.md; done + @node src/check_links.mjs + @echo Done. + +tex: $(foreach CHAP,$(CHAPTERS),pdf/$(CHAP).tex) pdf/hints.tex $(patsubst img/%.svg,img/generated/%.pdf,$(SVGS)) + +book.pdf: tex pdf/book.tex src/build_book_pdf.mjs + node src/build_book_pdf.mjs book book.pdf + cp book.pdf html/Mahir_JavaScript.pdf + +pdf/book_mobile.tex: pdf/book.tex src/build_mobile_tex.mjs + node src/build_mobile_tex.mjs pdf/book.tex pdf/book_mobile.tex + +book_mobile.pdf: pdf/book_mobile.tex tex src/build_book_pdf.mjs + node src/build_book_pdf.mjs book_mobile book_mobile.pdf + cp book_mobile.pdf html/Mahir_JavaScript_mobile.pdf + +pdf/hints.tex: $(foreach CHAP,$(CHAPTERS),$(CHAP).md) src/extract_hints.mjs + node src/extract_hints.mjs | node src/render_latex.mjs - > $@ + +img/generated/%.pdf: img/%.svg + inkscape --export-pdf=$@ $< + +pdf/%.tex: %.md + node src/render_latex.mjs $< > $@ + +book.epub: epub/titlepage.xhtml epub/toc.xhtml epub/hints.xhtml $(foreach CHAP,$(CHAPTERS),epub/$(CHAP).xhtml) \ + epub/content.opf.src epub/style.css src/build_epub.mjs + rm -f $@ + node src/build_epub.mjs $@ + cp book.epub html/Mahir_JavaScript.epub + +epub/toc.xhtml: epub/toc.xhtml.src $(foreach CHAP,$(CHAPTERS),epub/$(CHAP).xhtml) epub/hints.xhtml + node src/generate_epub_toc.mjs $^ > $@ + +epub/%.xhtml: %.md src/render_html.mjs + node src/render_html.mjs --epub $< > $@ + +epub/hints.xhtml: $(foreach CHAP,$(CHAPTERS),$(CHAP).md) src/extract_hints.mjs src/render_html.mjs + node src/extract_hints.mjs | node src/render_html.mjs --epub - > $@ + +epubcheck: book.epub + epubcheck book.epub 2>&1 | grep -v 'img/.*\.svg' + +book.mobi: book.epub img/cover.jpg + ebook-convert book.epub book.mobi --output-profile=kindle --cover=img/cover.jpg --remove-first-image + cp book.mobi html/Mahir_JavaScript.mobi diff --git a/README.md b/README.md index 6702073d4..13de511bf 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,39 @@ -# Eloquent JavaScript +# Mahir JavaScript -These are the sources used to build the fourth edition of Eloquent -JavaScript (https://eloquentjavascript.net). +Ini adalah sumber kode dan tulisan yang digunakan untuk membangun Mahir Javascript (https://mahirjavascript.net). -Feedback welcome, in the form of issues and pull requests. +Ini adalah terjemahan dari buku _Eloquent Javascript - Fourth Edition_ (https://eloquentjavascript.net). -## Building +Kami menyambut baik masukan dari anda, silakan membuat *issues* dan *pull request*. -This builds the HTML output in `html/`, where `make` is GNU make: +## Menyusun buku + +Untuk menyusun buku menjadi keluaran HTML di `html/`, anda bisa menggunakan make: npm install make html -To build the PDF file (don't bother trying this unless you really need -it, since this list has probably bitrotted again and getting all this -set up is a pain): +Untuk membuat berkas PDF (kami tidak menyarankannya kecuali anda sangat membutuhkannya, sebab daftar ini mungkin akan kadaluwarsa dan menyusun ulangnya agak sulit): apt-get install texlive texlive-xetex fonts-inconsolata fonts-symbola texlive-lang-chinese inkscape make book.pdf -## Translating +## Menerjemahkan -Translations are very much welcome. The license this book is published -under allows non-commercial derivations, which includes open -translations. If you do one, let me know, and I'll add a link to the -website. +Kami menyambut baik bantuan penerjemahan. Lisensi buku ini mengizinkan turunan non-komersial, termasuk terjemahan. Jika anda selesai menerjemahkan, kabari saya. -A note of caution though: This text consists of about 130 000 words, -the paper book is 400 pages. That's a lot of text, which will take a -lot of time to translate. +Peringatan: buku ini mengandung sekitar 130.000 kata, dan sekitar 400 halaman. Itu tulisan yang cukup banyak, yang akan membutuhkan waktu yang banyak untuk menerjemahkannya. -If that doesn't scare you off, the recommended way to go about a -translation is: +Jika itu tidak menakutkan bagi anda, cara terbaik untuk menerjemahkan tulisan ini adalah: - - Fork this repository on GitHub. + - *Fork* (Cabangkan) repositori ini di Github. - - Create an issue on the repository describing your plan to translate. + - Buat sebuah *Issue* di respositori ini yang menjelaskan rencana penerjemahan yang akan anda lakukan. - - Translate the `.md` files in your fork. These are - [CommonMark](https://commonmark.org/) formatted, with a few - extensions. You may consider omitting the index terms (indicated - with double parentheses and `{{index ...}}` syntax) from your - translation, since that's mostly relevant for print output. + - Terjemahkan berkas `.md` di percabangan anda. Berkas tersebut diformat menggunakan [CommonMark](https://commonmark.org), dengan beberapa tambahan ekstensi. Anda mungkin perlu mengecualikan istilah-istilah indeks (yang ditandai dengan kurung ganda dan sintaks `{{index ...}}`) dari terjemahan anda, karena hal itu lebih banyak berkaitan dengan keluaran cetak. - - Publish somewhere online or ask me to host the result. + - Publikasikan terjemahan anda secara daring atau silakan minta saya untuk memuatnya. -Doing this in public, and creating an issue that links to your work, -helps avoid wasted effort, where multiple people start a translation -to the same language (and possibly never finish it). (Since -translations have to retain the license, it is okay to pick up someone -else's translation and continue it, even when they have vanished from -the internet.) +Jika anda melakukan penerjemahan ini secara terbuka, dan membuat *issue* yang tertaut dengan pekerjaan anda, maka anda akan menghindari usaha yang sia-sia dimana beberapa orang yang berbeda memulai terjemahan untuk bahasa yang sama (dan mungkin tidak akan pernah menyelesaikannya). (Karena terjemahan memiliki lisensi yang sama, anda bebas untuk mengambil terjemahan orang lain dan melanjutkannya, bahkan jika terjemahan tersebut telah menghilang dari internet.) -I am not interested in machine translations. Please only ask me to -link your translation when it was done by actual people. +Saya tidak tertarik dengan terjemahan menggunakan mesin / komputer. Mohon hanya meminta saya untuk menautkan terjemahan anda jika itu dilakukan oleh manusia. diff --git a/exercise/1_chessboard.js b/exercise/1_chessboard.js new file mode 100644 index 000000000..44796079e --- /dev/null +++ b/exercise/1_chessboard.js @@ -0,0 +1,24 @@ +console.log("Chapter 1 - Chessboard") + +function chessboard(size) { + let board = "" + + for (let i = 0; i < size; i++) { + let row = "" + const isEvenRow = i % 2 == 0 + const evenChar = isEvenRow ? "#" : " " + const oddChar = isEvenRow ? " " : "#" + for (let j = 0; j < size; j++) { + if (j % 2 == 0) { + row += evenChar + } else { + row += oddChar + } + } + row += "\n" + board += row + } + return board +} + +console.log(chessboard(10)) diff --git a/exercise/1_fizzbuzz.js b/exercise/1_fizzbuzz.js new file mode 100644 index 000000000..8e35b2925 --- /dev/null +++ b/exercise/1_fizzbuzz.js @@ -0,0 +1,15 @@ +console.log("Chapter 1 - FizzBuzz") + +for (let i = 1; i <= 100; i++) { + const isFizz = i % 3 == 0 + const isBuzz = i % 5 == 0 + if (isFizz && isBuzz) { + console.log("FizzBuzz") + } else if (isFizz) { + console.log("Fizz") + } else if (isBuzz) { + console.log("Buzz") + } else { + console.log(i) + } +} diff --git a/exercise/1_pyramid.js b/exercise/1_pyramid.js new file mode 100644 index 000000000..2168eeadd --- /dev/null +++ b/exercise/1_pyramid.js @@ -0,0 +1,11 @@ +console.log("Chapter 1 - Pyramid") + +const level = 10 + +for (let i = 0; i < level; i++) { + let row = "# " + for (let j = 0; j < i; j++) { + row += "# " + } + console.log(row) +} diff --git a/html/Eloquent_JavaScript.epub b/html/Eloquent_JavaScript.epub deleted file mode 120000 index a9098be1e..000000000 --- a/html/Eloquent_JavaScript.epub +++ /dev/null @@ -1 +0,0 @@ -../book.epub \ No newline at end of file diff --git a/html/Eloquent_JavaScript.mobi b/html/Eloquent_JavaScript.mobi deleted file mode 120000 index b41e9cb51..000000000 --- a/html/Eloquent_JavaScript.mobi +++ /dev/null @@ -1 +0,0 @@ -../book.mobi \ No newline at end of file diff --git a/html/Eloquent_JavaScript.pdf b/html/Eloquent_JavaScript.pdf deleted file mode 120000 index 4da1f1afc..000000000 --- a/html/Eloquent_JavaScript.pdf +++ /dev/null @@ -1 +0,0 @@ -../book.pdf \ No newline at end of file diff --git a/html/Eloquent_JavaScript_small.pdf b/html/Eloquent_JavaScript_small.pdf deleted file mode 120000 index c28a0ae94..000000000 --- a/html/Eloquent_JavaScript_small.pdf +++ /dev/null @@ -1 +0,0 @@ -../book_mobile.pdf \ No newline at end of file diff --git a/html/Mahir_JavaScript.epub b/html/Mahir_JavaScript.epub new file mode 100644 index 000000000..158ea81d7 Binary files /dev/null and b/html/Mahir_JavaScript.epub differ diff --git a/html/Mahir_JavaScript.mobi b/html/Mahir_JavaScript.mobi new file mode 100644 index 000000000..9810e3ad2 Binary files /dev/null and b/html/Mahir_JavaScript.mobi differ diff --git a/html/Mahir_JavaScript.pdf b/html/Mahir_JavaScript.pdf new file mode 100644 index 000000000..c94462f52 Binary files /dev/null and b/html/Mahir_JavaScript.pdf differ diff --git a/html/Mahir_JavaScript_mobile.pdf b/html/Mahir_JavaScript_mobile.pdf new file mode 100644 index 000000000..901ed4f5b Binary files /dev/null and b/html/Mahir_JavaScript_mobile.pdf differ diff --git a/html/code b/html/code deleted file mode 120000 index 2edff2610..000000000 --- a/html/code +++ /dev/null @@ -1 +0,0 @@ -../code \ No newline at end of file diff --git a/html/code/LICENSE b/html/code/LICENSE new file mode 100644 index 000000000..c36bc0217 --- /dev/null +++ b/html/code/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2008-2024 by Marijn Haverbeke + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/html/code/_stop_keys.js b/html/code/_stop_keys.js new file mode 100644 index 000000000..bc5de4e01 --- /dev/null +++ b/html/code/_stop_keys.js @@ -0,0 +1,3 @@ +window.addEventListener("keydown", e => { + if (/Arrow|Home|End|Page/.test(e.key)) e.preventDefault() +}) diff --git a/html/code/animatevillage.js b/html/code/animatevillage.js new file mode 100644 index 000000000..3a4f2303f --- /dev/null +++ b/html/code/animatevillage.js @@ -0,0 +1,131 @@ +// test: no + +(function() { + "use strict" + + let active = null + + const places = { + "Alice's House": {x: 279, y: 100}, + "Bob's House": {x: 295, y: 203}, + "Cabin": {x: 372, y: 67}, + "Daria's House": {x: 183, y: 285}, + "Ernie's House": {x: 50, y: 283}, + "Farm": {x: 36, y: 118}, + "Grete's House": {x: 35, y: 187}, + "Marketplace": {x: 162, y: 110}, + "Post Office": {x: 205, y: 57}, + "Shop": {x: 137, y: 212}, + "Town Hall": {x: 202, y: 213} + } + const placeKeys = Object.keys(places) + + const speed = 2 + + class Animation { + constructor(worldState, robot, robotState) { + this.worldState = worldState + this.robot = robot + this.robotState = robotState + this.turn = 0 + + let outer = (window.__sandbox ? window.__sandbox.output.div : document.body), doc = outer.ownerDocument + this.node = outer.appendChild(doc.createElement("div")) + this.node.style.cssText = "position: relative; line-height: 0.1; margin-left: 10px" + this.map = this.node.appendChild(doc.createElement("img")) + this.imgPath = "img/" + if (/\/code($|\/)/.test(outer.ownerDocument.defaultView.location)) this.imgPath = "../" + this.imgPath + console.log(outer.ownerDocument.defaultView.location.toString(), /\/code($|\/)/.test(outer.ownerDocument.defaultView.localation), this.imgPath) + this.map.src = this.imgPath + "village2x.png" + this.map.style.cssText = "vertical-align: -8px" + this.robotElt = this.node.appendChild(doc.createElement("div")) + this.robotElt.style.cssText = `position: absolute; transition: left ${0.8 / speed}s, top ${0.8 / speed}s;` + let robotPic = this.robotElt.appendChild(doc.createElement("img")) + robotPic.src = this.imgPath + "robot_moving2x.gif" + this.parcels = [] + + this.text = this.node.appendChild(doc.createElement("span")) + this.button = this.node.appendChild(doc.createElement("button")) + this.button.style.cssText = "color: white; background: #28b; border: none; border-radius: 2px; padding: 2px 5px; line-height: 1.1; font-family: sans-serif; font-size: 80%" + this.button.textContent = "Stop" + + this.button.addEventListener("click", () => this.clicked()) + this.schedule() + + this.updateView() + this.updateParcels() + + this.robotElt.addEventListener("transitionend", () => this.updateParcels()) + } + + + updateView() { + let pos = places[this.worldState.place] + this.robotElt.style.top = (pos.y - 38) + "px" + this.robotElt.style.left = (pos.x - 16) + "px" + + this.text.textContent = ` Turn ${this.turn} ` + } + + updateParcels() { + while (this.parcels.length) this.parcels.pop().remove() + let heights = {} + for (let {place, address} of this.worldState.parcels) { + let height = heights[place] || (heights[place] = 0) + heights[place] += 14 + let node = document.createElement("div") + let offset = placeKeys.indexOf(address) * 16 + node.style.cssText = `position: absolute; height: 16px; width: 16px; background-image: url(${this.imgPath}parcel2x.png); background-position: 0 -${offset}px`; + if (place == this.worldState.place) { + node.style.left = "25px" + node.style.bottom = (20 + height) + "px" + this.robotElt.appendChild(node) + } else { + let pos = places[place] + node.style.left = (pos.x - 5) + "px" + node.style.top = (pos.y - 10 - height) + "px" + this.node.appendChild(node) + } + this.parcels.push(node) + } + } + + tick() { + let {direction, memory} = this.robot(this.worldState, this.robotState) + this.worldState = this.worldState.move(direction) + this.robotState = memory + this.turn++ + this.updateView() + if (this.worldState.parcels.length == 0) { + this.button.remove() + this.text.textContent = ` Finished after ${this.turn} turns` + this.robotElt.firstChild.src = this.imgPath + "robot_idle2x.png" + } else { + this.schedule() + } + } + + schedule() { + this.timeout = setTimeout(() => this.tick(), 1000 / speed) + } + + clicked() { + if (this.timeout == null) { + this.schedule() + this.button.textContent = "Stop" + this.robotElt.firstChild.src = this.imgPath + "robot_moving2x.gif" + } else { + clearTimeout(this.timeout) + this.timeout = null + this.button.textContent = "Start" + this.robotElt.firstChild.src = this.imgPath + "robot_idle2x.png" + } + } + } + + window.runRobotAnimation = function(worldState, robot, robotState) { + if (active && active.timeout != null) + clearTimeout(active.timeout) + active = new Animation(worldState, robot, robotState) + } +})() diff --git a/html/code/chapter/.keep b/html/code/chapter/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/html/code/chapter/04_data.js b/html/code/chapter/04_data.js new file mode 100644 index 000000000..f6af2a94b --- /dev/null +++ b/html/code/chapter/04_data.js @@ -0,0 +1,55 @@ +var journal = []; + +function addEntry(events, squirrel) { + journal.push({events, squirrel}); +} + +function phi(table) { + return (table[3] * table[0] - table[2] * table[1]) / + Math.sqrt((table[2] + table[3]) * + (table[0] + table[1]) * + (table[1] + table[3]) * + (table[0] + table[2])); +} + +function tableFor(event, journal) { + let table = [0, 0, 0, 0]; + for (let i = 0; i < journal.length; i++) { + let entry = journal[i], index = 0; + if (entry.events.includes(event)) index += 1; + if (entry.squirrel) index += 2; + table[index] += 1; + } + return table; +} + +function journalEvents(journal) { + let events = []; + for (let entry of journal) { + for (let event of entry.events) { + if (!events.includes(event)) { + events.push(event); + } + } + } + return events; +} + +function max(...numbers) { + let result = -Infinity; + for (let number of numbers) { + if (number > result) result = number; + } + return result; +} + +var list = { + value: 1, + rest: { + value: 2, + rest: { + value: 3, + rest: null + } + } +}; diff --git a/html/code/chapter/04_data.zip b/html/code/chapter/04_data.zip new file mode 100644 index 000000000..c9d903546 Binary files /dev/null and b/html/code/chapter/04_data.zip differ diff --git a/html/code/chapter/05_higher_order.js b/html/code/chapter/05_higher_order.js new file mode 100644 index 000000000..dfb34fd55 --- /dev/null +++ b/html/code/chapter/05_higher_order.js @@ -0,0 +1,44 @@ +function repeat(n, action) { + for (let i = 0; i < n; i++) { + action(i); + } +} + +function characterScript(code) { + for (let script of SCRIPTS) { + if (script.ranges.some(([from, to]) => { + return code >= from && code < to; + })) { + return script; + } + } + return null; +} + +function countBy(items, groupName) { + let counts = []; + for (let item of items) { + let name = groupName(item); + let known = counts.find(c => c.name == name); + if (!known) { + counts.push({name, count: 1}); + } else { + known.count++; + } + } + return counts; +} + +function textScripts(text) { + let scripts = countBy(text, char => { + let script = characterScript(char.codePointAt(0)); + return script ? script.name : "none"; + }).filter(({name}) => name != "none"); + + let total = scripts.reduce((n, {count}) => n + count, 0); + if (total == 0) return "No scripts found"; + + return scripts.map(({name, count}) => { + return `${Math.round(count * 100 / total)}% ${name}`; + }).join(", "); +} diff --git a/html/code/chapter/05_higher_order.zip b/html/code/chapter/05_higher_order.zip new file mode 100644 index 000000000..bb0e099cc Binary files /dev/null and b/html/code/chapter/05_higher_order.zip differ diff --git a/html/code/chapter/06_object.js b/html/code/chapter/06_object.js new file mode 100644 index 000000000..6aaec04eb --- /dev/null +++ b/html/code/chapter/06_object.js @@ -0,0 +1,99 @@ +function speak(line) { + console.log(`The ${this.type} rabbit says '${line}'`); +} +var whiteRabbit = {type: "white", speak}; +var hungryRabbit = {type: "hungry", speak}; + + +var protoRabbit = { + speak(line) { + console.log(`The ${this.type} rabbit says '${line}'`); + } +}; +var blackRabbit = Object.create(protoRabbit); +blackRabbit.type = "black"; + +var Rabbit = class Rabbit { + constructor(type) { + this.type = type; + } + speak(line) { + console.log(`The ${this.type} rabbit says '${line}'`); + } +} + +var killerRabbit = new Rabbit("killer"); + +Rabbit.prototype.toString = function() { + return `a ${this.type} rabbit`; +}; + +var Temperature = class Temperature { + constructor(celsius) { + this.celsius = celsius; + } + get fahrenheit() { + return this.celsius * 1.8 + 32; + } + set fahrenheit(value) { + this.celsius = (value - 32) / 1.8; + } + + static fromFahrenheit(value) { + return new Temperature((value - 32) / 1.8); + } +} + + +var length = Symbol("length"); + +var List = class List { + constructor(value, rest) { + this.value = value; + this.rest = rest; + } + + get length() { + return 1 + (this.rest ? this.rest.length : 0); + } + + static fromArray(array) { + let result = null; + for (let i = array.length - 1; i >= 0; i--) { + result = new this(array[i], result); + } + return result; + } +} + +var ListIterator = class ListIterator { + constructor(list) { + this.list = list; + } + + next() { + if (this.list == null) { + return {done: true}; + } + let value = this.list.value; + this.list = this.list.rest; + return {value, done: false}; + } +} + +List.prototype[Symbol.iterator] = function() { + return new ListIterator(this); +}; + +var LengthList = class LengthList extends List { + #length; + + constructor(value, rest) { + super(value, rest); + this.#length = super.length; + } + + get length() { + return this.#length; + } +} diff --git a/html/code/chapter/06_object.zip b/html/code/chapter/06_object.zip new file mode 100644 index 000000000..7435bc51d Binary files /dev/null and b/html/code/chapter/06_object.zip differ diff --git a/html/code/chapter/07_robot.js b/html/code/chapter/07_robot.js new file mode 100644 index 000000000..8d6e95bed --- /dev/null +++ b/html/code/chapter/07_robot.js @@ -0,0 +1,120 @@ +var roads = [ + "Alice's House-Bob's House", "Alice's House-Cabin", + "Alice's House-Post Office", "Bob's House-Town Hall", + "Daria's House-Ernie's House", "Daria's House-Town Hall", + "Ernie's House-Grete's House", "Grete's House-Farm", + "Grete's House-Shop", "Marketplace-Farm", + "Marketplace-Post Office", "Marketplace-Shop", + "Marketplace-Town Hall", "Shop-Town Hall" +]; + +function buildGraph(edges) { + let graph = Object.create(null); + function addEdge(from, to) { + if (from in graph) { + graph[from].push(to); + } else { + graph[from] = [to]; + } + } + for (let [from, to] of edges.map(r => r.split("-"))) { + addEdge(from, to); + addEdge(to, from); + } + return graph; +} + +var roadGraph = buildGraph(roads); + +var VillageState = class VillageState { + constructor(place, parcels) { + this.place = place; + this.parcels = parcels; + } + + move(destination) { + if (!roadGraph[this.place].includes(destination)) { + return this; + } else { + let parcels = this.parcels.map(p => { + if (p.place != this.place) return p; + return {place: destination, address: p.address}; + }).filter(p => p.place != p.address); + return new VillageState(destination, parcels); + } + } +} + +function runRobot(state, robot, memory) { + for (let turn = 0;; turn++) { + if (state.parcels.length == 0) { + console.log(`Done in ${turn} turns`); + break; + } + let action = robot(state, memory); + state = state.move(action.direction); + memory = action.memory; + console.log(`Moved to ${action.direction}`); + } +} + +function randomPick(array) { + let choice = Math.floor(Math.random() * array.length); + return array[choice]; +} + +function randomRobot(state) { + return {direction: randomPick(roadGraph[state.place])}; +} + +VillageState.random = function(parcelCount = 5) { + let parcels = []; + for (let i = 0; i < parcelCount; i++) { + let address = randomPick(Object.keys(roadGraph)); + let place; + do { + place = randomPick(Object.keys(roadGraph)); + } while (place == address); + parcels.push({place, address}); + } + return new VillageState("Post Office", parcels); +}; + +var mailRoute = [ + "Alice's House", "Cabin", "Alice's House", "Bob's House", + "Town Hall", "Daria's House", "Ernie's House", + "Grete's House", "Shop", "Grete's House", "Farm", + "Marketplace", "Post Office" +]; + +function routeRobot(state, memory) { + if (memory.length == 0) { + memory = mailRoute; + } + return {direction: memory[0], memory: memory.slice(1)}; +} + +function findRoute(graph, from, to) { + let work = [{at: from, route: []}]; + for (let i = 0; i < work.length; i++) { + let {at, route} = work[i]; + for (let place of graph[at]) { + if (place == to) return route.concat(place); + if (!work.some(w => w.at == place)) { + work.push({at: place, route: route.concat(place)}); + } + } + } +} + +function goalOrientedRobot({place, parcels}, route) { + if (route.length == 0) { + let parcel = parcels[0]; + if (parcel.place != place) { + route = findRoute(roadGraph, place, parcel.place); + } else { + route = findRoute(roadGraph, place, parcel.address); + } + } + return {direction: route[0], memory: route.slice(1)}; +} diff --git a/html/code/chapter/08_error.js b/html/code/chapter/08_error.js new file mode 100644 index 000000000..5fde85fe7 --- /dev/null +++ b/html/code/chapter/08_error.js @@ -0,0 +1,43 @@ +var accounts = { + a: 100, + b: 0, + c: 20 +}; + +function getAccount() { + let accountName = prompt("Enter an account name"); + if (!Object.hasOwn(accounts, accountName)) { + throw new Error(`No such account: ${accountName}`); + } + return accountName; +} + +function transfer(from, amount) { + if (accounts[from] < amount) return; + accounts[from] -= amount; + accounts[getAccount()] += amount; +} + +function transfer(from, amount) { + if (accounts[from] < amount) return; + let progress = 0; + try { + accounts[from] -= amount; + progress = 1; + accounts[getAccount()] += amount; + progress = 2; + } finally { + if (progress == 1) { + accounts[from] += amount; + } + } +} + +var InputError = class InputError extends Error {} + +function promptDirection(question) { + let result = prompt(question); + if (result.toLowerCase() == "left") return "L"; + if (result.toLowerCase() == "right") return "R"; + throw new InputError("Invalid direction: " + result); +} diff --git a/html/code/chapter/11_async.js b/html/code/chapter/11_async.js new file mode 100644 index 000000000..45fe402ac --- /dev/null +++ b/html/code/chapter/11_async.js @@ -0,0 +1,87 @@ +function textFile(filename) { + return new Promise((resolve, reject) => { + readTextFile(filename, (text, error) => { + if (error) reject(error); + else resolve(text); + }); + }); +} + +function withTimeout(promise, time) { + return new Promise((resolve, reject) => { + promise.then(resolve, reject); + setTimeout(() => reject("Timed out"), time); + }); +} + +function crackPasscode(networkID) { + function nextDigit(code, digit) { + let newCode = code + digit; + return withTimeout(joinWifi(networkID, newCode), 50) + .then(() => newCode) + .catch(failure => { + if (failure == "Timed out") { + return nextDigit(newCode, 0); + } else if (digit < 9) { + return nextDigit(code, digit + 1); + } else { + throw failure; + } + }); + } + return nextDigit("", 0); +} + +var Group = class Group { + constructor() { this.members = []; } + add(m) { this.members.add(m); } +} + +var screenAddresses = [ + "10.0.0.44", "10.0.0.45", "10.0.0.41", + "10.0.0.31", "10.0.0.40", "10.0.0.42", + "10.0.0.48", "10.0.0.47", "10.0.0.46" +]; + +function displayFrame(frame) { + return Promise.all(frame.map((data, i) => { + return request(screenAddresses[i], { + command: "display", + data + }); + })); +} + +function wait(time) { + return new Promise(accept => setTimeout(accept, time)); +} + +var VideoPlayer = class VideoPlayer { + constructor(frames, frameTime) { + this.frames = frames; + this.frameTime = frameTime; + this.stopped = true; + } + + async play() { + this.stopped = false; + for (let i = 0; !this.stopped; i++) { + let nextFrame = wait(this.frameTime); + await displayFrame(this.frames[i % this.frames.length]); + await nextFrame; + } + } + + stop() { + this.stopped = true; + } +} + +async function fileSizes(files) { + let list = ""; + await Promise.all(files.map(async fileName => { + list += fileName + ": " + + (await textFile(fileName)).length + "\n"; + })); + return list; +} diff --git a/html/code/chapter/11_async.zip b/html/code/chapter/11_async.zip new file mode 100644 index 000000000..2bdd2ce45 Binary files /dev/null and b/html/code/chapter/11_async.zip differ diff --git a/html/code/chapter/12_language.js b/html/code/chapter/12_language.js new file mode 100644 index 000000000..806a16f0f --- /dev/null +++ b/html/code/chapter/12_language.js @@ -0,0 +1,163 @@ +function parseExpression(program) { + program = skipSpace(program); + let match, expr; + if (match = /^"([^"]*)"/.exec(program)) { + expr = {type: "value", value: match[1]}; + } else if (match = /^\d+\b/.exec(program)) { + expr = {type: "value", value: Number(match[0])}; + } else if (match = /^[^\s(),#"]+/.exec(program)) { + expr = {type: "word", name: match[0]}; + } else { + throw new SyntaxError("Unexpected syntax: " + program); + } + + return parseApply(expr, program.slice(match[0].length)); +} + +function skipSpace(string) { + let first = string.search(/\S/); + if (first == -1) return ""; + return string.slice(first); +} + +function parseApply(expr, program) { + program = skipSpace(program); + if (program[0] != "(") { + return {expr: expr, rest: program}; + } + + program = skipSpace(program.slice(1)); + expr = {type: "apply", operator: expr, args: []}; + while (program[0] != ")") { + let arg = parseExpression(program); + expr.args.push(arg.expr); + program = skipSpace(arg.rest); + if (program[0] == ",") { + program = skipSpace(program.slice(1)); + } else if (program[0] != ")") { + throw new SyntaxError("Expected ',' or ')'"); + } + } + return parseApply(expr, program.slice(1)); +} + +function parse(program) { + let {expr, rest} = parseExpression(program); + if (skipSpace(rest).length > 0) { + throw new SyntaxError("Unexpected text after program"); + } + return expr; +} +// operator: {type: "word", name: "+"}, +// args: [{type: "word", name: "a"}, +// {type: "value", value: 10}]} + +var specialForms = Object.create(null); + +function evaluate(expr, scope) { + if (expr.type == "value") { + return expr.value; + } else if (expr.type == "word") { + if (expr.name in scope) { + return scope[expr.name]; + } else { + throw new ReferenceError( + `Undefined binding: ${expr.name}`); + } + } else if (expr.type == "apply") { + let {operator, args} = expr; + if (operator.type == "word" && + operator.name in specialForms) { + return specialForms[operator.name](expr.args, scope); + } else { + let op = evaluate(operator, scope); + if (typeof op == "function") { + return op(...args.map(arg => evaluate(arg, scope))); + } else { + throw new TypeError("Applying a non-function."); + } + } + } +} + +specialForms.if = (args, scope) => { + if (args.length != 3) { + throw new SyntaxError("Wrong number of args to if"); + } else if (evaluate(args[0], scope) !== false) { + return evaluate(args[1], scope); + } else { + return evaluate(args[2], scope); + } +}; + +specialForms.while = (args, scope) => { + if (args.length != 2) { + throw new SyntaxError("Wrong number of args to while"); + } + while (evaluate(args[0], scope) !== false) { + evaluate(args[1], scope); + } + + // Since undefined does not exist in Egg, we return false, + // for lack of a meaningful result + return false; +}; + +specialForms.do = (args, scope) => { + let value = false; + for (let arg of args) { + value = evaluate(arg, scope); + } + return value; +}; + +specialForms.define = (args, scope) => { + if (args.length != 2 || args[0].type != "word") { + throw new SyntaxError("Incorrect use of define"); + } + let value = evaluate(args[1], scope); + scope[args[0].name] = value; + return value; +}; + +var topScope = Object.create(null); + +topScope.true = true; +topScope.false = false; + +for (let op of ["+", "-", "*", "/", "==", "<", ">"]) { + topScope[op] = Function("a, b", `return a ${op} b;`); +} + +topScope.print = value => { + console.log(value); + return value; +}; + +function run(program) { + return evaluate(parse(program), Object.create(topScope)); +} + +specialForms.fun = (args, scope) => { + if (!args.length) { + throw new SyntaxError("Functions need a body"); + } + let body = args[args.length - 1]; + let params = args.slice(0, args.length - 1).map(expr => { + if (expr.type != "word") { + throw new SyntaxError("Parameter names must be words"); + } + return expr.name; + }); + + return function(...args) { + if (args.length != params.length) { + throw new TypeError("Wrong number of arguments"); + } + let localScope = Object.create(scope); + for (let i = 0; i < args.length; i++) { + localScope[params[i]] = args[i]; + } + return evaluate(body, localScope); + }; +}; diff --git a/html/code/chapter/12_language.zip b/html/code/chapter/12_language.zip new file mode 100644 index 000000000..e9bc00858 Binary files /dev/null and b/html/code/chapter/12_language.zip differ diff --git a/html/code/chapter/16_game.js b/html/code/chapter/16_game.js new file mode 100644 index 000000000..54a0f7233 --- /dev/null +++ b/html/code/chapter/16_game.js @@ -0,0 +1,361 @@ +var simpleLevelPlan = ` +...................... +..#................#.. +..#..............=.#.. +..#.........o.o....#.. +..#.@......#####...#.. +..#####............#.. +......#++++++++++++#.. +......##############.. +......................`; + +var Level = class Level { + constructor(plan) { + let rows = plan.trim().split("\n").map(l => [...l]); + this.height = rows.length; + this.width = rows[0].length; + this.startActors = []; + + this.rows = rows.map((row, y) => { + return row.map((ch, x) => { + let type = levelChars[ch]; + if (typeof type != "string") { + let pos = new Vec(x, y); + this.startActors.push(type.create(pos, ch)); + type = "empty"; + } + return type; + }); + }); + } +} + +var State = class State { + constructor(level, actors, status) { + this.level = level; + this.actors = actors; + this.status = status; + } + + static start(level) { + return new State(level, level.startActors, "playing"); + } + + get player() { + return this.actors.find(a => a.type == "player"); + } +} + +var Vec = class Vec { + constructor(x, y) { + this.x = x; this.y = y; + } + plus(other) { + return new Vec(this.x + other.x, this.y + other.y); + } + times(factor) { + return new Vec(this.x * factor, this.y * factor); + } +} + +var Player = class Player { + constructor(pos, speed) { + this.pos = pos; + this.speed = speed; + } + + get type() { return "player"; } + + static create(pos) { + return new Player(pos.plus(new Vec(0, -0.5)), + new Vec(0, 0)); + } +} + +Player.prototype.size = new Vec(0.8, 1.5); + +var Lava = class Lava { + constructor(pos, speed, reset) { + this.pos = pos; + this.speed = speed; + this.reset = reset; + } + + get type() { return "lava"; } + + static create(pos, ch) { + if (ch == "=") { + return new Lava(pos, new Vec(2, 0)); + } else if (ch == "|") { + return new Lava(pos, new Vec(0, 2)); + } else if (ch == "v") { + return new Lava(pos, new Vec(0, 3), pos); + } + } +} + +Lava.prototype.size = new Vec(1, 1); + +var Coin = class Coin { + constructor(pos, basePos, wobble) { + this.pos = pos; + this.basePos = basePos; + this.wobble = wobble; + } + + get type() { return "coin"; } + + static create(pos) { + let basePos = pos.plus(new Vec(0.2, 0.1)); + return new Coin(basePos, basePos, + Math.random() * Math.PI * 2); + } +} + +Coin.prototype.size = new Vec(0.6, 0.6); + +var levelChars = { + ".": "empty", "#": "wall", "+": "lava", + "@": Player, "o": Coin, + "=": Lava, "|": Lava, "v": Lava +}; + +var simpleLevel = new Level(simpleLevelPlan); + +function elt(name, attrs, ...children) { + let dom = document.createElement(name); + for (let attr of Object.keys(attrs)) { + dom.setAttribute(attr, attrs[attr]); + } + for (let child of children) { + dom.appendChild(child); + } + return dom; +} + +var DOMDisplay = class DOMDisplay { + constructor(parent, level) { + this.dom = elt("div", {class: "game"}, drawGrid(level)); + this.actorLayer = null; + parent.appendChild(this.dom); + } + + clear() { this.dom.remove(); } +} + +var scale = 20; + +function drawGrid(level) { + return elt("table", { + class: "background", + style: `width: ${level.width * scale}px` + }, ...level.rows.map(row => + elt("tr", {style: `height: ${scale}px`}, + ...row.map(type => elt("td", {class: type}))) + )); +} + +function drawActors(actors) { + return elt("div", {}, ...actors.map(actor => { + let rect = elt("div", {class: `actor ${actor.type}`}); + rect.style.width = `${actor.size.x * scale}px`; + rect.style.height = `${actor.size.y * scale}px`; + rect.style.left = `${actor.pos.x * scale}px`; + rect.style.top = `${actor.pos.y * scale}px`; + return rect; + })); +} + +DOMDisplay.prototype.syncState = function(state) { + if (this.actorLayer) this.actorLayer.remove(); + this.actorLayer = drawActors(state.actors); + this.dom.appendChild(this.actorLayer); + this.dom.className = `game ${state.status}`; + this.scrollPlayerIntoView(state); +}; + +DOMDisplay.prototype.scrollPlayerIntoView = function(state) { + let width = this.dom.clientWidth; + let height = this.dom.clientHeight; + let margin = width / 3; + + // The viewport + let left = this.dom.scrollLeft, right = left + width; + let top = this.dom.scrollTop, bottom = top + height; + + let player = state.player; + let center = player.pos.plus(player.size.times(0.5)) + .times(scale); + + if (center.x < left + margin) { + this.dom.scrollLeft = center.x - margin; + } else if (center.x > right - margin) { + this.dom.scrollLeft = center.x + margin - width; + } + if (center.y < top + margin) { + this.dom.scrollTop = center.y - margin; + } else if (center.y > bottom - margin) { + this.dom.scrollTop = center.y + margin - height; + } +}; + +Level.prototype.touches = function(pos, size, type) { + let xStart = Math.floor(pos.x); + let xEnd = Math.ceil(pos.x + size.x); + let yStart = Math.floor(pos.y); + let yEnd = Math.ceil(pos.y + size.y); + + for (let y = yStart; y < yEnd; y++) { + for (let x = xStart; x < xEnd; x++) { + let isOutside = x < 0 || x >= this.width || + y < 0 || y >= this.height; + let here = isOutside ? "wall" : this.rows[y][x]; + if (here == type) return true; + } + } + return false; +}; + +State.prototype.update = function(time, keys) { + let actors = this.actors + .map(actor => actor.update(time, this, keys)); + let newState = new State(this.level, actors, this.status); + + if (newState.status != "playing") return newState; + + let player = newState.player; + if (this.level.touches(player.pos, player.size, "lava")) { + return new State(this.level, actors, "lost"); + } + + for (let actor of actors) { + if (actor != player && overlap(actor, player)) { + newState = actor.collide(newState); + } + } + return newState; +}; + +function overlap(actor1, actor2) { + return actor1.pos.x + actor1.size.x > actor2.pos.x && + actor1.pos.x < actor2.pos.x + actor2.size.x && + actor1.pos.y + actor1.size.y > actor2.pos.y && + actor1.pos.y < actor2.pos.y + actor2.size.y; +} + +Lava.prototype.collide = function(state) { + return new State(state.level, state.actors, "lost"); +}; + +Coin.prototype.collide = function(state) { + let filtered = state.actors.filter(a => a != this); + let status = state.status; + if (!filtered.some(a => a.type == "coin")) status = "won"; + return new State(state.level, filtered, status); +}; + +Lava.prototype.update = function(time, state) { + let newPos = this.pos.plus(this.speed.times(time)); + if (!state.level.touches(newPos, this.size, "wall")) { + return new Lava(newPos, this.speed, this.reset); + } else if (this.reset) { + return new Lava(this.reset, this.speed, this.reset); + } else { + return new Lava(this.pos, this.speed.times(-1)); + } +}; + +var wobbleSpeed = 8, wobbleDist = 0.07; + +Coin.prototype.update = function(time) { + let wobble = this.wobble + time * wobbleSpeed; + let wobblePos = Math.sin(wobble) * wobbleDist; + return new Coin(this.basePos.plus(new Vec(0, wobblePos)), + this.basePos, wobble); +}; + +var playerXSpeed = 7; +var gravity = 30; +var jumpSpeed = 17; + +Player.prototype.update = function(time, state, keys) { + let xSpeed = 0; + if (keys.ArrowLeft) xSpeed -= playerXSpeed; + if (keys.ArrowRight) xSpeed += playerXSpeed; + let pos = this.pos; + let movedX = pos.plus(new Vec(xSpeed * time, 0)); + if (!state.level.touches(movedX, this.size, "wall")) { + pos = movedX; + } + + let ySpeed = this.speed.y + time * gravity; + let movedY = pos.plus(new Vec(0, ySpeed * time)); + if (!state.level.touches(movedY, this.size, "wall")) { + pos = movedY; + } else if (keys.ArrowUp && ySpeed > 0) { + ySpeed = -jumpSpeed; + } else { + ySpeed = 0; + } + return new Player(pos, new Vec(xSpeed, ySpeed)); +}; + +function trackKeys(keys) { + let down = Object.create(null); + function track(event) { + if (keys.includes(event.key)) { + down[event.key] = event.type == "keydown"; + event.preventDefault(); + } + } + window.addEventListener("keydown", track); + window.addEventListener("keyup", track); + return down; +} + +var arrowKeys = + trackKeys(["ArrowLeft", "ArrowRight", "ArrowUp"]); + +function runAnimation(frameFunc) { + let lastTime = null; + function frame(time) { + if (lastTime != null) { + let timeStep = Math.min(time - lastTime, 100) / 1000; + if (frameFunc(timeStep) === false) return; + } + lastTime = time; + requestAnimationFrame(frame); + } + requestAnimationFrame(frame); +} + +function runLevel(level, Display) { + let display = new Display(document.body, level); + let state = State.start(level); + let ending = 1; + return new Promise(resolve => { + runAnimation(time => { + state = state.update(time, arrowKeys); + display.syncState(state); + if (state.status == "playing") { + return true; + } else if (ending > 0) { + ending -= time; + return true; + } else { + display.clear(); + resolve(state.status); + return false; + } + }); + }); +} + +async function runGame(plans, Display) { + for (let level = 0; level < plans.length;) { + let status = await runLevel(new Level(plans[level]), + Display); + if (status == "won") level++; + } + console.log("You've won!"); +} diff --git a/html/code/chapter/16_game.zip b/html/code/chapter/16_game.zip new file mode 100644 index 000000000..aeb1db635 Binary files /dev/null and b/html/code/chapter/16_game.zip differ diff --git a/html/code/chapter/17_canvas.js b/html/code/chapter/17_canvas.js new file mode 100644 index 000000000..dd6ee70f4 --- /dev/null +++ b/html/code/chapter/17_canvas.js @@ -0,0 +1,143 @@ +var results = [ + {name: "Satisfied", count: 1043, color: "lightblue"}, + {name: "Neutral", count: 563, color: "lightgreen"}, + {name: "Unsatisfied", count: 510, color: "pink"}, + {name: "No comment", count: 175, color: "silver"} +]; + +function flipHorizontally(context, around) { + context.translate(around, 0); + context.scale(-1, 1); + context.translate(-around, 0); +} + +var CanvasDisplay = class CanvasDisplay { + constructor(parent, level) { + this.canvas = document.createElement("canvas"); + this.canvas.width = Math.min(600, level.width * scale); + this.canvas.height = Math.min(450, level.height * scale); + parent.appendChild(this.canvas); + this.cx = this.canvas.getContext("2d"); + + this.flipPlayer = false; + + this.viewport = { + left: 0, + top: 0, + width: this.canvas.width / scale, + height: this.canvas.height / scale + }; + } + + clear() { + this.canvas.remove(); + } +} + +CanvasDisplay.prototype.syncState = function(state) { + this.updateViewport(state); + this.clearDisplay(state.status); + this.drawBackground(state.level); + this.drawActors(state.actors); +}; + +CanvasDisplay.prototype.updateViewport = function(state) { + let view = this.viewport, margin = view.width / 3; + let player = state.player; + let center = player.pos.plus(player.size.times(0.5)); + + if (center.x < view.left + margin) { + view.left = Math.max(center.x - margin, 0); + } else if (center.x > view.left + view.width - margin) { + view.left = Math.min(center.x + margin - view.width, + state.level.width - view.width); + } + if (center.y < view.top + margin) { + view.top = Math.max(center.y - margin, 0); + } else if (center.y > view.top + view.height - margin) { + view.top = Math.min(center.y + margin - view.height, + state.level.height - view.height); + } +}; + +CanvasDisplay.prototype.clearDisplay = function(status) { + if (status == "won") { + this.cx.fillStyle = "rgb(68, 191, 255)"; + } else if (status == "lost") { + this.cx.fillStyle = "rgb(44, 136, 214)"; + } else { + this.cx.fillStyle = "rgb(52, 166, 251)"; + } + this.cx.fillRect(0, 0, + this.canvas.width, this.canvas.height); +}; + +var otherSprites = document.createElement("img"); +otherSprites.src = "img/sprites.png"; + +CanvasDisplay.prototype.drawBackground = function(level) { + let {left, top, width, height} = this.viewport; + let xStart = Math.floor(left); + let xEnd = Math.ceil(left + width); + let yStart = Math.floor(top); + let yEnd = Math.ceil(top + height); + + for (let y = yStart; y < yEnd; y++) { + for (let x = xStart; x < xEnd; x++) { + let tile = level.rows[y][x]; + if (tile == "empty") continue; + let screenX = (x - left) * scale; + let screenY = (y - top) * scale; + let tileX = tile == "lava" ? scale : 0; + this.cx.drawImage(otherSprites, + tileX, 0, scale, scale, + screenX, screenY, scale, scale); + } + } +}; + +var playerSprites = document.createElement("img"); +playerSprites.src = "img/player.png"; +var playerXOverlap = 4; + +CanvasDisplay.prototype.drawPlayer = function(player, x, y, + width, height){ + width += playerXOverlap * 2; + x -= playerXOverlap; + if (player.speed.x != 0) { + this.flipPlayer = player.speed.x < 0; + } + + let tile = 8; + if (player.speed.y != 0) { + tile = 9; + } else if (player.speed.x != 0) { + tile = Math.floor(Date.now() / 60) % 8; + } + + this.cx.save(); + if (this.flipPlayer) { + flipHorizontally(this.cx, x + width / 2); + } + let tileX = tile * width; + this.cx.drawImage(playerSprites, tileX, 0, width, height, + x, y, width, height); + this.cx.restore(); +}; + +CanvasDisplay.prototype.drawActors = function(actors) { + for (let actor of actors) { + let width = actor.size.x * scale; + let height = actor.size.y * scale; + let x = (actor.pos.x - this.viewport.left) * scale; + let y = (actor.pos.y - this.viewport.top) * scale; + if (actor.type == "player") { + this.drawPlayer(actor, x, y, width, height); + } else { + let tileX = (actor.type == "coin" ? 2 : 1) * scale; + this.cx.drawImage(otherSprites, + tileX, 0, width, height, + x, y, width, height); + } + } +}; diff --git a/html/code/chapter/17_canvas.zip b/html/code/chapter/17_canvas.zip new file mode 100644 index 000000000..f854a19cb Binary files /dev/null and b/html/code/chapter/17_canvas.zip differ diff --git a/html/code/chapter/19_paint.js b/html/code/chapter/19_paint.js new file mode 100644 index 000000000..b3198d12c --- /dev/null +++ b/html/code/chapter/19_paint.js @@ -0,0 +1,345 @@ +var Picture = class Picture { + constructor(width, height, pixels) { + this.width = width; + this.height = height; + this.pixels = pixels; + } + static empty(width, height, color) { + let pixels = new Array(width * height).fill(color); + return new Picture(width, height, pixels); + } + pixel(x, y) { + return this.pixels[x + y * this.width]; + } + draw(pixels) { + let copy = this.pixels.slice(); + for (let {x, y, color} of pixels) { + copy[x + y * this.width] = color; + } + return new Picture(this.width, this.height, copy); + } +} + +function updateState(state, action) { + return {...state, ...action}; +} + +function elt(type, props, ...children) { + let dom = document.createElement(type); + if (props) Object.assign(dom, props); + for (let child of children) { + if (typeof child != "string") dom.appendChild(child); + else dom.appendChild(document.createTextNode(child)); + } + return dom; +} + +var scale = 10; + +var PictureCanvas = class PictureCanvas { + constructor(picture, pointerDown) { + this.dom = elt("canvas", { + onmousedown: event => this.mouse(event, pointerDown), + ontouchstart: event => this.touch(event, pointerDown) + }); + this.syncState(picture); + } + syncState(picture) { + if (this.picture == picture) return; + this.picture = picture; + drawPicture(this.picture, this.dom, scale); + } +} + +function drawPicture(picture, canvas, scale) { + canvas.width = picture.width * scale; + canvas.height = picture.height * scale; + let cx = canvas.getContext("2d"); + + for (let y = 0; y < picture.height; y++) { + for (let x = 0; x < picture.width; x++) { + cx.fillStyle = picture.pixel(x, y); + cx.fillRect(x * scale, y * scale, scale, scale); + } + } +} + +PictureCanvas.prototype.mouse = function(downEvent, onDown) { + if (downEvent.button != 0) return; + let pos = pointerPosition(downEvent, this.dom); + let onMove = onDown(pos); + if (!onMove) return; + let move = moveEvent => { + if (moveEvent.buttons == 0) { + this.dom.removeEventListener("mousemove", move); + } else { + let newPos = pointerPosition(moveEvent, this.dom); + if (newPos.x == pos.x && newPos.y == pos.y) return; + pos = newPos; + onMove(newPos); + } + }; + this.dom.addEventListener("mousemove", move); +}; + +function pointerPosition(pos, domNode) { + let rect = domNode.getBoundingClientRect(); + return {x: Math.floor((pos.clientX - rect.left) / scale), + y: Math.floor((pos.clientY - rect.top) / scale)}; +} + +PictureCanvas.prototype.touch = function(startEvent, + onDown) { + let pos = pointerPosition(startEvent.touches[0], this.dom); + let onMove = onDown(pos); + startEvent.preventDefault(); + if (!onMove) return; + let move = moveEvent => { + let newPos = pointerPosition(moveEvent.touches[0], + this.dom); + if (newPos.x == pos.x && newPos.y == pos.y) return; + pos = newPos; + onMove(newPos); + }; + let end = () => { + this.dom.removeEventListener("touchmove", move); + this.dom.removeEventListener("touchend", end); + }; + this.dom.addEventListener("touchmove", move); + this.dom.addEventListener("touchend", end); +}; + +var PixelEditor = class PixelEditor { + constructor(state, config) { + let {tools, controls, dispatch} = config; + this.state = state; + + this.canvas = new PictureCanvas(state.picture, pos => { + let tool = tools[this.state.tool]; + let onMove = tool(pos, this.state, dispatch); + if (onMove) return pos => onMove(pos, this.state); + }); + this.controls = controls.map( + Control => new Control(state, config)); + this.dom = elt("div", {}, this.canvas.dom, elt("br"), + ...this.controls.reduce( + (a, c) => a.concat(" ", c.dom), [])); + } + syncState(state) { + this.state = state; + this.canvas.syncState(state.picture); + for (let ctrl of this.controls) ctrl.syncState(state); + } +} + +var ToolSelect = class ToolSelect { + constructor(state, {tools, dispatch}) { + this.select = elt("select", { + onchange: () => dispatch({tool: this.select.value}) + }, ...Object.keys(tools).map(name => elt("option", { + selected: name == state.tool + }, name))); + this.dom = elt("label", null, "🖌 Tool: ", this.select); + } + syncState(state) { this.select.value = state.tool; } +} + +var ColorSelect = class ColorSelect { + constructor(state, {dispatch}) { + this.input = elt("input", { + type: "color", + value: state.color, + onchange: () => dispatch({color: this.input.value}) + }); + this.dom = elt("label", null, "🎨 Color: ", this.input); + } + syncState(state) { this.input.value = state.color; } +} + +function draw(pos, state, dispatch) { + function drawPixel({x, y}, state) { + let drawn = {x, y, color: state.color}; + dispatch({picture: state.picture.draw([drawn])}); + } + drawPixel(pos, state); + return drawPixel; +} + +function rectangle(start, state, dispatch) { + function drawRectangle(pos) { + let xStart = Math.min(start.x, pos.x); + let yStart = Math.min(start.y, pos.y); + let xEnd = Math.max(start.x, pos.x); + let yEnd = Math.max(start.y, pos.y); + let drawn = []; + for (let y = yStart; y <= yEnd; y++) { + for (let x = xStart; x <= xEnd; x++) { + drawn.push({x, y, color: state.color}); + } + } + dispatch({picture: state.picture.draw(drawn)}); + } + drawRectangle(start); + return drawRectangle; +} + +var around = [{dx: -1, dy: 0}, {dx: 1, dy: 0}, + {dx: 0, dy: -1}, {dx: 0, dy: 1}]; + +function fill({x, y}, state, dispatch) { + let targetColor = state.picture.pixel(x, y); + let drawn = [{x, y, color: state.color}]; + let visited = new Set(); + for (let done = 0; done < drawn.length; done++) { + for (let {dx, dy} of around) { + let x = drawn[done].x + dx, y = drawn[done].y + dy; + if (x >= 0 && x < state.picture.width && + y >= 0 && y < state.picture.height && + !visited.has(x + "," + y) && + state.picture.pixel(x, y) == targetColor) { + drawn.push({x, y, color: state.color}); + visited.add(x + "," + y); + } + } + } + dispatch({picture: state.picture.draw(drawn)}); +} + +function pick(pos, state, dispatch) { + dispatch({color: state.picture.pixel(pos.x, pos.y)}); +} + +var SaveButton = class SaveButton { + constructor(state) { + this.picture = state.picture; + this.dom = elt("button", { + onclick: () => this.save() + }, "💾 Save"); + } + save() { + let canvas = elt("canvas"); + drawPicture(this.picture, canvas, 1); + let link = elt("a", { + href: canvas.toDataURL(), + download: "pixelart.png" + }); + document.body.appendChild(link); + link.click(); + link.remove(); + } + syncState(state) { this.picture = state.picture; } +} + +var LoadButton = class LoadButton { + constructor(_, {dispatch}) { + this.dom = elt("button", { + onclick: () => startLoad(dispatch) + }, "📁 Load"); + } + syncState() {} +} + +function startLoad(dispatch) { + let input = elt("input", { + type: "file", + onchange: () => finishLoad(input.files[0], dispatch) + }); + document.body.appendChild(input); + input.click(); + input.remove(); +} + +function finishLoad(file, dispatch) { + if (file == null) return; + let reader = new FileReader(); + reader.addEventListener("load", () => { + let image = elt("img", { + onload: () => dispatch({ + picture: pictureFromImage(image) + }), + src: reader.result + }); + }); + reader.readAsDataURL(file); +} + +function pictureFromImage(image) { + let width = Math.min(100, image.width); + let height = Math.min(100, image.height); + let canvas = elt("canvas", {width, height}); + let cx = canvas.getContext("2d"); + cx.drawImage(image, 0, 0); + let pixels = []; + let {data} = cx.getImageData(0, 0, width, height); + + function hex(n) { + return n.toString(16).padStart(2, "0"); + } + for (let i = 0; i < data.length; i += 4) { + let [r, g, b] = data.slice(i, i + 3); + pixels.push("#" + hex(r) + hex(g) + hex(b)); + } + return new Picture(width, height, pixels); +} + +function historyUpdateState(state, action) { + if (action.undo == true) { + if (state.done.length == 0) return state; + return { + ...state, + picture: state.done[0], + done: state.done.slice(1), + doneAt: 0 + }; + } else if (action.picture && + state.doneAt < Date.now() - 1000) { + return { + ...state, + ...action, + done: [state.picture, ...state.done], + doneAt: Date.now() + }; + } else { + return {...state, ...action}; + } +} + +var UndoButton = class UndoButton { + constructor(state, {dispatch}) { + this.dom = elt("button", { + onclick: () => dispatch({undo: true}), + disabled: state.done.length == 0 + }, "⮪ Undo"); + } + syncState(state) { + this.dom.disabled = state.done.length == 0; + } +} + +var startState = { + tool: "draw", + color: "#000000", + picture: Picture.empty(60, 30, "#f0f0f0"), + done: [], + doneAt: 0 +}; + +var baseTools = {draw, fill, rectangle, pick}; + +var baseControls = [ + ToolSelect, ColorSelect, SaveButton, LoadButton, UndoButton +]; + +function startPixelEditor({state = startState, + tools = baseTools, + controls = baseControls}) { + let app = new PixelEditor(state, { + tools, + controls, + dispatch(action) { + state = historyUpdateState(state, action); + app.syncState(state); + } + }); + return app.dom; +} diff --git a/html/code/chapter/19_paint.zip b/html/code/chapter/19_paint.zip new file mode 100644 index 000000000..703397105 Binary files /dev/null and b/html/code/chapter/19_paint.zip differ diff --git a/html/code/chapter/22_fast.js b/html/code/chapter/22_fast.js new file mode 100644 index 000000000..8e0d12ad3 --- /dev/null +++ b/html/code/chapter/22_fast.js @@ -0,0 +1,144 @@ +var Graph = class Graph { + #nodes = []; + + get size() { + return this.#nodes.length; + } + + addNode() { + let id = this.#nodes.length; + this.#nodes.push(new Set()); + return id; + } + + addEdge(nodeA, nodeB) { + this.#nodes[nodeA].add(nodeB); + this.#nodes[nodeB].add(nodeA); + } + + neighbors(node) { + return this.#nodes[node]; + } +} + +function randomLayout(graph) { + let layout = []; + for (let i = 0; i < graph.size; i++) { + layout.push(new Vec(Math.random() * 1000, + Math.random() * 1000)); + } + return layout; +} + +function gridGraph(size) { + let grid = new Graph(); + for (let y = 0; y < size; y++) { + for (let x = 0; x < size; x++) { + let id = grid.addNode(); + if (x > 0) grid.addEdge(id, id - 1); + if (y > 0) grid.addEdge(id, id - size); + } + } + return grid; +} + +var springLength = 20; +var springStrength = 0.1; +var repulsionStrength = 1500; + +function forceSize(distance, connected) { + let repulse = -repulsionStrength / (distance * distance); + let spring = 0; + if (connected) { + spring = (distance - springLength) * springStrength; + } + return spring + repulse; +} + +function forceDirected_simple(layout, graph) { + for (let a = 0; a < graph.size; a++) { + for (let b = 0; b < graph.size; b++) { + if (a == b) continue; + let apart = layout[b].minus(layout[a]); + let distance = Math.max(1, apart.length); + let connected = graph.neighbors(a).has(b); + let size = forceSize(distance, connected); + let force = apart.times(1 / distance).times(size); + layout[a] = layout[a].plus(force); + } + } +} + +function pause() { + return new Promise(done => setTimeout(done, 0)) +} + +async function runLayout(implementation, graph) { + let time = 0, iterations = 0; + let layout = randomLayout(graph); + while (time < 3000) { + let start = Date.now(); + for (let i = 0; i < 100; i++) { + implementation(layout, graph); + iterations++; + } + time += Date.now() - start; + drawGraph(graph, layout); + await pause(); + } + let perSecond = Math.round(iterations / (time / 1000)); + console.log(`${perSecond} iterations per second`); +} + +function forceDirected_noRepeat(layout, graph) { + for (let a = 0; a < graph.size; a++) { + for (let b = a + 1; b < graph.size; b++) { + let apart = layout[b].minus(layout[a]); + let distance = Math.max(1, apart.length); + let connected = graph.neighbors(a).has(b); + let size = forceSize(distance, connected); + let force = apart.times(1 / distance).times(size); + layout[a] = layout[a].plus(force); + layout[b] = layout[b].minus(force); + } + } +} + +var skipDistance = 175; + +function forceDirected_skip(layout, graph) { + for (let a = 0; a < graph.size; a++) { + for (let b = a + 1; b < graph.size; b++) { + let apart = layout[b].minus(layout[a]); + let distance = Math.max(1, apart.length); + let connected = graph.neighbors(a).has(b); + if (distance > skipDistance && !connected) continue; + let size = forceSize(distance, connected); + let force = apart.times(1 / distance).times(size); + layout[a] = layout[a].plus(force); + layout[b] = layout[b].minus(force); + } + } +} + +function forceDirected_noVector(layout, graph) { + for (let a = 0; a < graph.size; a++) { + let posA = layout[a]; + for (let b = a + 1; b < graph.size; b++) { + let posB = layout[b]; + let apartX = posB.x - posA.x + let apartY = posB.y - posA.y; + let distance = Math.sqrt(apartX * apartX + + apartY * apartY); + let connected = graph.neighbors(a).has(b); + if (distance > skipDistance && !connected) continue; + let size = forceSize(distance, connected); + let forceX = (apartX / distance) * size; + let forceY = (apartY / distance) * size; + posA.x += forceX; + posA.y += forceY; + posB.x -= forceX; + posB.y -= forceY; + } + } +} diff --git a/html/code/chapter_info.js b/html/code/chapter_info.js new file mode 100644 index 000000000..2ef5cde64 --- /dev/null +++ b/html/code/chapter_info.js @@ -0,0 +1,781 @@ +var chapterData = [ + { + "number": 0, + "id": "00_pendahuluan", + "title": "Pendahuluan", + "start_code": "console.log(sum(range(1, 10)));\n", + "exercises": [], + "include": [ + "code/intro.js" + ] + }, + { + "number": 1, + "id": "01_values", + "title": "Values, Types, and Operators", + "start_code": "", + "exercises": [], + "include": null + }, + { + "number": 2, + "id": "02_program_structure", + "title": "Program Structure", + "start_code": "", + "exercises": [ + { + "name": "Looping a triangle", + "file": "code/solutions/02_1_looping_a_triangle.js", + "number": 1, + "type": "js", + "code": "// Your code here.", + "solution": "for (let line = \"#\"; line.length < 8; line += \"#\")\n console.log(line);" + }, + { + "name": "FizzBuzz", + "file": "code/solutions/02_2_fizzbuzz.js", + "number": 2, + "type": "js", + "code": "// Your code here.", + "solution": "for (let n = 1; n <= 100; n++) {\n let output = \"\";\n if (n % 3 == 0) output += \"Fizz\";\n if (n % 5 == 0) output += \"Buzz\";\n console.log(output || n);\n}" + }, + { + "name": "Chessboard", + "file": "code/solutions/02_3_chessboard.js", + "number": 3, + "type": "js", + "code": "// Your code here.", + "solution": "let size = 8;\n\nlet board = \"\";\n\nfor (let y = 0; y < size; y++) {\n for (let x = 0; x < size; x++) {\n if ((x + y) % 2 == 0) {\n board += \" \";\n } else {\n board += \"#\";\n }\n }\n board += \"\\n\";\n}\n\nconsole.log(board);" + } + ], + "include": null + }, + { + "number": 3, + "id": "03_functions", + "title": "Functions", + "start_code": "", + "exercises": [ + { + "name": "Minimum", + "file": "code/solutions/03_1_minimum.js", + "number": 1, + "type": "js", + "code": "// Your code here.\n\nconsole.log(min(0, 10));\n// → 0\nconsole.log(min(0, -10));\n// → -10", + "solution": "function min(a, b) {\n if (a < b) return a;\n else return b;\n}\n\nconsole.log(min(0, 10));\n// → 0\nconsole.log(min(0, -10));\n// → -10" + }, + { + "name": "Recursion", + "file": "code/solutions/03_2_recursion.js", + "number": 2, + "type": "js", + "code": "// Your code here.\n\nconsole.log(isEven(50));\n// → true\nconsole.log(isEven(75));\n// → false\nconsole.log(isEven(-1));\n// → ??", + "solution": "function isEven(n) {\n if (n == 0) return true;\n else if (n == 1) return false;\n else if (n < 0) return isEven(-n);\n else return isEven(n - 2);\n}\n\nconsole.log(isEven(50));\n// → true\nconsole.log(isEven(75));\n// → false\nconsole.log(isEven(-1));\n// → false" + }, + { + "name": "Bean counting", + "file": "code/solutions/03_3_bean_counting.js", + "number": 3, + "type": "js", + "code": "// Your code here.\n\nconsole.log(countBs(\"BOB\"));\n// → 2\nconsole.log(countChar(\"kakkerlak\", \"k\"));\n// → 4", + "solution": "function countChar(string, ch) {\n let counted = 0;\n for (let i = 0; i < string.length; i++) {\n if (string[i] == ch) {\n counted += 1;\n }\n }\n return counted;\n}\n\nfunction countBs(string) {\n return countChar(string, \"B\");\n}\n\nconsole.log(countBs(\"BBC\"));\n// → 2\nconsole.log(countChar(\"kakkerlak\", \"k\"));\n// → 4" + } + ], + "include": null + }, + { + "number": 4, + "id": "04_data", + "title": "Data Structures: Objects and Arrays", + "start_code": "for (let event of journalEvents(JOURNAL)) {\n let correlation = phi(tableFor(event, JOURNAL));\n if (correlation > 0.1 || correlation < -0.1) {\n console.log(event + \":\", correlation);\n }\n}\n// → brushed teeth: -0.3805211953\n// → work: -0.1371988681\n// → reading: 0.1106828054\n", + "exercises": [ + { + "name": "The sum of a range", + "file": "code/solutions/04_1_the_sum_of_a_range.js", + "number": 1, + "type": "js", + "code": "// Your code here.\n\nconsole.log(range(1, 10));\n// → [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]\nconsole.log(range(5, 2, -1));\n// → [5, 4, 3, 2]\nconsole.log(sum(range(1, 10)));\n// → 55", + "solution": "function range(start, end, step = start < end ? 1 : -1) {\n let array = [];\n\n if (step > 0) {\n for (let i = start; i <= end; i += step) array.push(i);\n } else {\n for (let i = start; i >= end; i += step) array.push(i);\n }\n return array;\n}\n\nfunction sum(array) {\n let total = 0;\n for (let value of array) {\n total += value;\n }\n return total;\n}\n\nconsole.log(range(1, 10))\n// → [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]\nconsole.log(range(5, 2, -1));\n// → [5, 4, 3, 2]\nconsole.log(sum(range(1, 10)));\n// → 55" + }, + { + "name": "Reversing an array", + "file": "code/solutions/04_2_reversing_an_array.js", + "number": 2, + "type": "js", + "code": "// Your code here.\n\nlet myArray = [\"A\", \"B\", \"C\"];\nconsole.log(reverseArray(myArray));\n// → [\"C\", \"B\", \"A\"];\nconsole.log(myArray);\n// → [\"A\", \"B\", \"C\"];\nlet arrayValue = [1, 2, 3, 4, 5];\nreverseArrayInPlace(arrayValue);\nconsole.log(arrayValue);\n// → [5, 4, 3, 2, 1]", + "solution": "function reverseArray(array) {\n let output = [];\n for (let i = array.length - 1; i >= 0; i--) {\n output.push(array[i]);\n }\n return output;\n}\n\nfunction reverseArrayInPlace(array) {\n for (let i = 0; i < Math.floor(array.length / 2); i++) {\n let old = array[i];\n array[i] = array[array.length - 1 - i];\n array[array.length - 1 - i] = old;\n }\n return array;\n}\n\nconsole.log(reverseArray([\"A\", \"B\", \"C\"]));\n// → [\"C\", \"B\", \"A\"];\nlet arrayValue = [1, 2, 3, 4, 5];\nreverseArrayInPlace(arrayValue);\nconsole.log(arrayValue);\n// → [5, 4, 3, 2, 1]" + }, + { + "name": "A list", + "file": "code/solutions/04_3_a_list.js", + "number": 3, + "type": "js", + "code": "// Your code here.\n\nconsole.log(arrayToList([10, 20]));\n// → {value: 10, rest: {value: 20, rest: null}}\nconsole.log(listToArray(arrayToList([10, 20, 30])));\n// → [10, 20, 30]\nconsole.log(prepend(10, prepend(20, null)));\n// → {value: 10, rest: {value: 20, rest: null}}\nconsole.log(nth(arrayToList([10, 20, 30]), 1));\n// → 20", + "solution": "function arrayToList(array) {\n let list = null;\n for (let i = array.length - 1; i >= 0; i--) {\n list = {value: array[i], rest: list};\n }\n return list;\n}\n\nfunction listToArray(list) {\n let array = [];\n for (let node = list; node; node = node.rest) {\n array.push(node.value);\n }\n return array;\n}\n\nfunction prepend(value, list) {\n return {value, rest: list};\n}\n\nfunction nth(list, n) {\n if (!list) return undefined;\n else if (n == 0) return list.value;\n else return nth(list.rest, n - 1);\n}\n\nconsole.log(arrayToList([10, 20]));\n// → {value: 10, rest: {value: 20, rest: null}}\nconsole.log(listToArray(arrayToList([10, 20, 30])));\n// → [10, 20, 30]\nconsole.log(prepend(10, prepend(20, null)));\n// → {value: 10, rest: {value: 20, rest: null}}\nconsole.log(nth(arrayToList([10, 20, 30]), 1));\n// → 20" + }, + { + "name": "Deep comparison", + "file": "code/solutions/04_4_deep_comparison.js", + "number": 4, + "type": "js", + "code": "// Your code here.\n\nlet obj = {here: {is: \"an\"}, object: 2};\nconsole.log(deepEqual(obj, obj));\n// → true\nconsole.log(deepEqual(obj, {here: 1, object: 2}));\n// → false\nconsole.log(deepEqual(obj, {here: {is: \"an\"}, object: 2}));\n// → true", + "solution": "function deepEqual(a, b) {\n if (a === b) return true;\n \n if (a == null || typeof a != \"object\" ||\n b == null || typeof b != \"object\") return false;\n\n let keysA = Object.keys(a), keysB = Object.keys(b);\n\n if (keysA.length != keysB.length) return false;\n\n for (let key of keysA) {\n if (!keysB.includes(key) || !deepEqual(a[key], b[key])) return false;\n }\n\n return true;\n}\n\nlet obj = {here: {is: \"an\"}, object: 2};\nconsole.log(deepEqual(obj, obj));\n// → true\nconsole.log(deepEqual(obj, {here: 1, object: 2}));\n// → false\nconsole.log(deepEqual(obj, {here: {is: \"an\"}, object: 2}));\n// → true" + } + ], + "include": [ + "code/journal.js", + "code/chapter/04_data.js" + ], + "links": [ + "code/chapter/04_data.zip" + ] + }, + { + "number": 5, + "id": "05_higher_order", + "title": "Higher-Order Functions", + "start_code": "function textScripts(text) {\n let scripts = countBy(text, char => {\n let script = characterScript(char.codePointAt(0));\n return script ? script.name : \"none\";\n }).filter(({name}) => name != \"none\");\n\n let total = scripts.reduce((n, {count}) => n + count, 0);\n if (total == 0) return \"No scripts found\";\n\n return scripts.map(({name, count}) => {\n return `${Math.round(count * 100 / total)}% ${name}`;\n }).join(\", \");\n}\n\nconsole.log(textScripts('英国的狗说\"woof\", 俄罗斯的狗说\"тяв\"'));\n", + "exercises": [ + { + "name": "Flattening", + "file": "code/solutions/05_1_flattening.js", + "number": 1, + "type": "js", + "code": "let arrays = [[1, 2, 3], [4, 5], [6]];\n// Your code here.\n// → [1, 2, 3, 4, 5, 6]", + "solution": "let arrays = [[1, 2, 3], [4, 5], [6]];\n\nconsole.log(arrays.reduce((flat, current) => flat.concat(current), []));\n// → [1, 2, 3, 4, 5, 6]" + }, + { + "name": "Your own loop", + "file": "code/solutions/05_2_your_own_loop.js", + "number": 2, + "type": "js", + "code": "// Your code here.\n\nloop(3, n => n > 0, n => n - 1, console.log);\n// → 3\n// → 2\n// → 1", + "solution": "function loop(start, test, update, body) {\n for (let value = start; test(value); value = update(value)) {\n body(value);\n }\n}\n\nloop(3, n => n > 0, n => n - 1, console.log);\n// → 3\n// → 2\n// → 1" + }, + { + "name": "Everything", + "file": "code/solutions/05_3_everything.js", + "number": 3, + "type": "js", + "code": "function every(array, test) {\n // Your code here.\n}\n\nconsole.log(every([1, 3, 5], n => n < 10));\n// → true\nconsole.log(every([2, 4, 16], n => n < 10));\n// → false\nconsole.log(every([], n => n < 10));\n// → true", + "solution": "function every(array, predicate) {\n for (let element of array) {\n if (!predicate(element)) return false;\n }\n return true;\n}\n\nfunction every2(array, predicate) {\n return !array.some(element => !predicate(element));\n}\n\nconsole.log(every([1, 3, 5], n => n < 10));\n// → true\nconsole.log(every([2, 4, 16], n => n < 10));\n// → false\nconsole.log(every([], n => n < 10));\n// → true" + }, + { + "name": "Dominant writing direction", + "file": "code/solutions/05_4_dominant_writing_direction.js", + "number": 4, + "type": "js", + "code": "function dominantDirection(text) {\n // Your code here.\n}\n\nconsole.log(dominantDirection(\"Hello!\"));\n// → ltr\nconsole.log(dominantDirection(\"Hey, مساء الخير\"));\n// → rtl", + "solution": "function dominantDirection(text) {\n let counted = countBy(text, char => {\n let script = characterScript(char.codePointAt(0));\n return script ? script.direction : \"none\";\n }).filter(({name}) => name != \"none\");\n\n if (counted.length == 0) return \"ltr\";\n\n return counted.reduce((a, b) => a.count > b.count ? a : b).name;\n}\n\nconsole.log(dominantDirection(\"Hello!\"));\n// → ltr\nconsole.log(dominantDirection(\"Hey, مساء الخير\"));\n// → rtl" + } + ], + "include": [ + "code/scripts.js", + "code/chapter/05_higher_order.js", + "code/intro.js" + ], + "links": [ + "code/chapter/05_higher_order.zip" + ] + }, + { + "number": 6, + "id": "06_object", + "title": "The Secret Life of Objects", + "start_code": "class Temperature {\n constructor(celsius) {\n this.celsius = celsius;\n }\n get fahrenheit() {\n return this.celsius * 1.8 + 32;\n }\n set fahrenheit(value) {\n this.celsius = (value - 32) / 1.8;\n }\n\n static fromFahrenheit(value) {\n return new Temperature((value - 32) / 1.8);\n }\n}\n\nlet temp = new Temperature(22);\nconsole.log(temp.fahrenheit);\ntemp.fahrenheit = 86;\nconsole.log(temp.celsius);\n", + "exercises": [ + { + "name": "A vector type", + "file": "code/solutions/06_1_a_vector_type.js", + "number": 1, + "type": "js", + "code": "// Your code here.\n\nconsole.log(new Vec(1, 2).plus(new Vec(2, 3)));\n// → Vec{x: 3, y: 5}\nconsole.log(new Vec(1, 2).minus(new Vec(2, 3)));\n// → Vec{x: -1, y: -1}\nconsole.log(new Vec(3, 4).length);\n// → 5", + "solution": "class Vec {\n constructor(x, y) {\n this.x = x;\n this.y = y;\n }\n\n plus(other) {\n return new Vec(this.x + other.x, this.y + other.y);\n }\n\n minus(other) {\n return new Vec(this.x - other.x, this.y - other.y);\n }\n\n get length() {\n return Math.sqrt(this.x * this.x + this.y * this.y);\n }\n}\n\nconsole.log(new Vec(1, 2).plus(new Vec(2, 3)));\n// → Vec{x: 3, y: 5}\nconsole.log(new Vec(1, 2).minus(new Vec(2, 3)));\n// → Vec{x: -1, y: -1}\nconsole.log(new Vec(3, 4).length);\n// → 5" + }, + { + "name": "Groups", + "file": "code/solutions/06_2_groups.js", + "number": 2, + "type": "js", + "code": "class Group {\n // Your code here.\n}\n\nlet group = Group.from([10, 20]);\nconsole.log(group.has(10));\n// → true\nconsole.log(group.has(30));\n// → false\ngroup.add(10);\ngroup.delete(10);\nconsole.log(group.has(10));\n// → false", + "solution": "class Group {\n #members = [];\n\n add(value) {\n if (!this.has(value)) {\n this.#members.push(value);\n }\n }\n\n delete(value) {\n this.#members = this.#members.filter(v => v !== value);\n }\n\n has(value) {\n return this.#members.includes(value);\n }\n\n static from(collection) {\n let group = new Group;\n for (let value of collection) {\n group.add(value);\n }\n return group;\n }\n}\n\nlet group = Group.from([10, 20]);\nconsole.log(group.has(10));\n// → true\nconsole.log(group.has(30));\n// → false\ngroup.add(10);\ngroup.delete(10);\nconsole.log(group.has(10));\n// → false" + }, + { + "name": "Iterable groups", + "file": "code/solutions/06_3_iterable_groups.js", + "number": 3, + "type": "js", + "code": "// Your code here (and the code from the previous exercise)\n\nfor (let value of Group.from([\"a\", \"b\", \"c\"])) {\n console.log(value);\n}\n// → a\n// → b\n// → c", + "solution": "class Group {\n #members = [];\n\n add(value) {\n if (!this.has(value)) {\n this.#members.push(value);\n }\n }\n\n delete(value) {\n this.#members = this.#members.filter(v => v !== value);\n }\n\n has(value) {\n return this.#members.includes(value);\n }\n\n static from(collection) {\n let group = new Group;\n for (let value of collection) {\n group.add(value);\n }\n return group;\n }\n\n [Symbol.iterator]() {\n return new GroupIterator(this.#members);\n }\n}\n\nclass GroupIterator {\n #members;\n #position;\n\n constructor(members) {\n this.#members = members;\n this.#position = 0;\n }\n\n next() {\n if (this.#position >= this.#members.length) {\n return {done: true};\n } else {\n let result = {value: this.#members[this.#position],\n done: false};\n this.#position++;\n return result;\n }\n }\n}\n\nfor (let value of Group.from([\"a\", \"b\", \"c\"])) {\n console.log(value);\n}\n// → a\n// → b\n// → c" + }, + { + "name": "Borrowing a method [3rd ed]", + "file": "code/solutions/06_4_borrowing_a_method.js", + "number": "4[3]", + "type": "js", + "code": "let map = {one: true, two: true, hasOwnProperty: true};\n\n// Fix this call\nconsole.log(map.hasOwnProperty(\"one\"));\n// → true", + "solution": "let map = {one: true, two: true, hasOwnProperty: true};\n\nconsole.log(Object.prototype.hasOwnProperty.call(map, \"one\"));\n// → true" + } + ], + "include": [ + "code/chapter/06_object.js" + ], + "links": [ + "code/chapter/06_object.zip" + ] + }, + { + "number": 7, + "id": "07_robot", + "title": "Project: A Robot", + "start_code": "runRobotAnimation(VillageState.random(),\n goalOrientedRobot, []);\n", + "exercises": [ + { + "name": "Measuring a robot", + "file": "code/solutions/07_1_measuring_a_robot.js", + "number": 1, + "type": "js", + "code": "function compareRobots(robot1, memory1, robot2, memory2) {\n // Your code here\n}\n\ncompareRobots(routeRobot, [], goalOrientedRobot, []);", + "solution": "function countSteps(state, robot, memory) {\n for (let steps = 0;; steps++) {\n if (state.parcels.length == 0) return steps;\n let action = robot(state, memory);\n state = state.move(action.direction);\n memory = action.memory;\n }\n}\n\nfunction compareRobots(robot1, memory1, robot2, memory2) {\n let total1 = 0, total2 = 0;\n for (let i = 0; i < 100; i++) {\n let state = VillageState.random();\n total1 += countSteps(state, robot1, memory1);\n total2 += countSteps(state, robot2, memory2);\n }\n console.log(`Robot 1 needed ${total1 / 100} steps per task`)\n console.log(`Robot 2 needed ${total2 / 100}`)\n}\n\ncompareRobots(routeRobot, [], goalOrientedRobot, []);" + }, + { + "name": "Robot efficiency", + "file": "code/solutions/07_2_robot_efficiency.js", + "number": 2, + "type": "js", + "code": "// Your code here\n\nrunRobotAnimation(VillageState.random(), yourRobot, memory);", + "solution": "function lazyRobot({place, parcels}, route) {\n if (route.length == 0) {\n // Describe a route for every parcel\n let routes = parcels.map(parcel => {\n if (parcel.place != place) {\n return {route: findRoute(roadGraph, place, parcel.place),\n pickUp: true};\n } else {\n return {route: findRoute(roadGraph, place, parcel.address),\n pickUp: false};\n }\n });\n\n // This determines the precedence a route gets when choosing.\n // Route length counts negatively, routes that pick up a package\n // get a small bonus.\n function score({route, pickUp}) {\n return (pickUp ? 0.5 : 0) - route.length;\n }\n route = routes.reduce((a, b) => score(a) > score(b) ? a : b).route;\n }\n\n return {direction: route[0], memory: route.slice(1)};\n}\n\nrunRobotAnimation(VillageState.random(), lazyRobot, []);" + }, + { + "name": "Persistent group", + "file": "code/solutions/07_3_persistent_group.js", + "number": 3, + "type": "js", + "code": "class PGroup {\n // Your code here\n}\n\nlet a = PGroup.empty.add(\"a\");\nlet ab = a.add(\"b\");\nlet b = ab.delete(\"a\");\n\nconsole.log(b.has(\"b\"));\n// → true\nconsole.log(a.has(\"b\"));\n// → false\nconsole.log(b.has(\"a\"));\n// → false", + "solution": "class PGroup {\n #members;\n constructor(members) {\n this.#members = members;\n }\n\n add(value) {\n if (this.has(value)) return this;\n return new PGroup(this.#members.concat([value]));\n }\n\n delete(value) {\n if (!this.has(value)) return this;\n return new PGroup(this.#members.filter(m => m !== value));\n }\n\n has(value) {\n return this.#members.includes(value);\n }\n\n static empty = new PGroup([]);\n}\n\nlet a = PGroup.empty.add(\"a\");\nlet ab = a.add(\"b\");\nlet b = ab.delete(\"a\");\n\nconsole.log(b.has(\"b\"));\n// → true\nconsole.log(a.has(\"b\"));\n// → false\nconsole.log(b.has(\"a\"));\n// → false" + } + ], + "include": [ + "code/chapter/07_robot.js", + "code/animatevillage.js" + ] + }, + { + "number": 8, + "id": "08_error", + "title": "Bugs and Errors", + "start_code": "", + "exercises": [ + { + "name": "Retry", + "file": "code/solutions/08_1_retry.js", + "number": 1, + "type": "js", + "code": "class MultiplicatorUnitFailure extends Error {}\n\nfunction primitiveMultiply(a, b) {\n if (Math.random() < 0.2) {\n return a * b;\n } else {\n throw new MultiplicatorUnitFailure(\"Klunk\");\n }\n}\n\nfunction reliableMultiply(a, b) {\n // Your code here.\n}\n\nconsole.log(reliableMultiply(8, 8));\n// → 64", + "solution": "class MultiplicatorUnitFailure extends Error {}\n\nfunction primitiveMultiply(a, b) {\n if (Math.random() < 0.2) {\n return a * b;\n } else {\n throw new MultiplicatorUnitFailure(\"Klunk\");\n }\n}\n\nfunction reliableMultiply(a, b) {\n for (;;) {\n try {\n return primitiveMultiply(a, b);\n } catch (e) {\n if (!(e instanceof MultiplicatorUnitFailure))\n throw e;\n }\n }\n}\n\nconsole.log(reliableMultiply(8, 8));\n// → 64" + }, + { + "name": "The locked box", + "file": "code/solutions/08_2_the_locked_box.js", + "number": 2, + "type": "js", + "code": "const box = new class {\n locked = true;\n #content = [];\n\n unlock() { this.locked = false; }\n lock() { this.locked = true; }\n get content() {\n if (this.locked) throw new Error(\"Locked!\");\n return this.#content;\n }\n};\n\nfunction withBoxUnlocked(body) {\n // Your code here.\n}\n\nwithBoxUnlocked(() => {\n box.content.push(\"gold piece\");\n});\n\ntry {\n withBoxUnlocked(() => {\n throw new Error(\"Pirates on the horizon! Abort!\");\n });\n} catch (e) {\n console.log(\"Error raised: \" + e);\n}\nconsole.log(box.locked);\n// → true", + "solution": "const box = new class {\n locked = true;\n #content = [];\n\n unlock() { this.locked = false; }\n lock() { this.locked = true; }\n get content() {\n if (this.locked) throw new Error(\"Locked!\");\n return this.#content;\n }\n};\n\nfunction withBoxUnlocked(body) {\n let locked = box.locked;\n if (locked) box.unlock();\n try {\n return body();\n } finally {\n if (locked) box.lock();\n }\n}\n\nwithBoxUnlocked(() => {\n box.content.push(\"gold piece\");\n});\n\ntry {\n withBoxUnlocked(() => {\n throw new Error(\"Pirates on the horizon! Abort!\");\n });\n} catch (e) {\n console.log(\"Error raised:\", e);\n}\n\nconsole.log(box.locked);\n// → true" + } + ], + "include": [ + "code/chapter/08_error.js" + ] + }, + { + "number": 9, + "id": "09_regexp", + "title": "Regular Expressions", + "start_code": "function parseINI(string) {\n // Start with an object to hold the top-level fields\n let result = {};\n let section = result;\n for (let line of string.split(/\\r?\\n/)) {\n let match;\n if (match = line.match(/^(\\w+)=(.*)$/)) {\n section[match[1]] = match[2];\n } else if (match = line.match(/^\\[(.*)\\]$/)) {\n section = result[match[1]] = {};\n } else if (!/^\\s*(;|$)/.test(line)) {\n throw new Error(\"Line '\" + line + \"' is not valid.\");\n }\n };\n return result;\n}\n\nconsole.log(parseINI(`\nname=Vasilis\n[address]\ncity=Tessaloniki`));\n", + "exercises": [ + { + "name": "Regexp golf", + "file": "code/solutions/09_1_regexp_golf.js", + "number": 1, + "type": "js", + "code": "// Fill in the regular expressions\n\nverify(/.../,\n [\"my car\", \"bad cats\"],\n [\"camper\", \"high art\"]);\n\nverify(/.../,\n [\"pop culture\", \"mad props\"],\n [\"plop\", \"prrrop\"]);\n\nverify(/.../,\n [\"ferret\", \"ferry\", \"ferrari\"],\n [\"ferrum\", \"transfer A\"]);\n\nverify(/.../,\n [\"how delicious\", \"spacious room\"],\n [\"ruinous\", \"consciousness\"]);\n\nverify(/.../,\n [\"bad punctuation .\"],\n [\"escape the period\"]);\n\nverify(/.../,\n [\"Siebentausenddreihundertzweiundzwanzig\"],\n [\"no\", \"three small words\"]);\n\nverify(/.../,\n [\"red platypus\", \"wobbling nest\"],\n [\"earth bed\", \"bedrøvet abe\", \"BEET\"]);\n\n\nfunction verify(regexp, yes, no) {\n // Ignore unfinished exercises\n if (regexp.source == \"...\") return;\n for (let str of yes) if (!regexp.test(str)) {\n console.log(`Failure to match '${str}'`);\n }\n for (let str of no) if (regexp.test(str)) {\n console.log(`Unexpected match for '${str}'`);\n }\n}", + "solution": "// Fill in the regular expressions\n\nverify(/ca[rt]/,\n [\"my car\", \"bad cats\"],\n [\"camper\", \"high art\"]);\n\nverify(/pr?op/,\n [\"pop culture\", \"mad props\"],\n [\"plop\", \"prrrop\"]);\n\nverify(/ferr(et|y|ari)/,\n [\"ferret\", \"ferry\", \"ferrari\"],\n [\"ferrum\", \"transfer A\"]);\n\nverify(/ious($|\\P{L})/u,\n [\"how delicious\", \"spacious room\"],\n [\"ruinous\", \"consciousness\"]);\n\nverify(/\\s[.,:;]/,\n [\"bad punctuation .\"],\n [\"escape the dot\"]);\n\nverify(/\\p{L}{7}/u,\n [\"Siebentausenddreihundertzweiundzwanzig\"],\n [\"no\", \"three small words\"]);\n\nverify(/(^|\\P{L})[^\\P{L}e]+($|\\P{L})/ui,\n [\"red platypus\", \"wobbling nest\"],\n [\"earth bed\", \"bedrøvet abe\", \"BEET\"]);\n\n\nfunction verify(regexp, yes, no) {\n // Ignore unfinished exercises\n if (regexp.source == \"...\") return;\n for (let str of yes) if (!regexp.test(str)) {\n console.log(`Failure to match '${str}'`);\n }\n for (let str of no) if (regexp.test(str)) {\n console.log(`Unexpected match for '${str}'`);\n }\n}" + }, + { + "name": "Quoting style", + "file": "code/solutions/09_2_quoting_style.js", + "number": 2, + "type": "js", + "code": "let text = \"'I'm the cook,' he said, 'it's my job.'\";\n// Change this call.\nconsole.log(text.replace(/A/g, \"B\"));\n// → \"I'm the cook,\" he said, \"it's my job.\"", + "solution": "let text = \"'I'm the cook,' he said, 'it's my job.'\";\n\nconsole.log(text.replace(/(^|\\P{L})'|'(\\P{L}|$)/gu, '$1\"$2'));\n// → \"I'm the cook,\" he said, \"it's my job.\"" + }, + { + "name": "Numbers again", + "file": "code/solutions/09_3_numbers_again.js", + "number": 3, + "type": "js", + "code": "// Fill in this regular expression.\nlet number = /^...$/;\n\n// Tests:\nfor (let str of [\"1\", \"-1\", \"+15\", \"1.55\", \".5\", \"5.\",\n \"1.3e2\", \"1E-4\", \"1e+12\"]) {\n if (!number.test(str)) {\n console.log(`Failed to match '${str}'`);\n }\n}\nfor (let str of [\"1a\", \"+-1\", \"1.2.3\", \"1+1\", \"1e4.5\",\n \".5.\", \"1f5\", \".\"]) {\n if (number.test(str)) {\n console.log(`Incorrectly accepted '${str}'`);\n }\n}", + "solution": "// Fill in this regular expression.\nlet number = /^[+\\-]?(\\d+(\\.\\d*)?|\\.\\d+)([eE][+\\-]?\\d+)?$/;\n\n// Tests:\nfor (let str of [\"1\", \"-1\", \"+15\", \"1.55\", \".5\", \"5.\",\n \"1.3e2\", \"1E-4\", \"1e+12\"]) {\n if (!number.test(str)) {\n console.log(`Failed to match '${str}'`);\n }\n}\nfor (let str of [\"1a\", \"+-1\", \"1.2.3\", \"1+1\", \"1e4.5\",\n \".5.\", \"1f5\", \".\"]) {\n if (number.test(str)) {\n console.log(`Incorrectly accepted '${str}'`);\n }\n}" + } + ], + "include": null + }, + { + "number": 10, + "id": "10_modules", + "title": "Modules", + "start_code": "", + "exercises": [ + { + "name": "Roads module", + "file": "code/solutions/10_2_roads_module.js", + "number": 2, + "type": "js", + "code": "// Add dependencies and exports\n\nconst roads = [\n \"Alice's House-Bob's House\", \"Alice's House-Cabin\",\n \"Alice's House-Post Office\", \"Bob's House-Town Hall\",\n \"Daria's House-Ernie's House\", \"Daria's House-Town Hall\",\n \"Ernie's House-Grete's House\", \"Grete's House-Farm\",\n \"Grete's House-Shop\", \"Marketplace-Farm\",\n \"Marketplace-Post Office\", \"Marketplace-Shop\",\n \"Marketplace-Town Hall\", \"Shop-Town Hall\"\n];", + "solution": "import {buildGraph} from \"./graph\";\n\nconst roads = [\n \"Alice's House-Bob's House\", \"Alice's House-Cabin\",\n \"Alice's House-Post Office\", \"Bob's House-Town Hall\",\n \"Daria's House-Ernie's House\", \"Daria's House-Town Hall\",\n \"Ernie's House-Grete's House\", \"Grete's House-Farm\",\n \"Grete's House-Shop\", \"Marketplace-Farm\",\n \"Marketplace-Post Office\", \"Marketplace-Shop\",\n \"Marketplace-Town Hall\", \"Shop-Town Hall\"\n];\n\nexport const roadGraph = buildGraph(roads.map(r => r.split(\"-\")));" + } + ], + "include": [ + "code/packages_chapter_10.js", + "code/chapter/07_robot.js" + ] + }, + { + "number": 11, + "id": "11_async", + "title": "Asynchronous Programming", + "start_code": "let video = new VideoPlayer(clipImages, 100);\nvideo.play().catch(e => {\n console.log(\"Playback failed: \" + e);\n});\nsetTimeout(() => video.stop(), 15000);\n", + "exercises": [ + { + "name": "Quiet Times", + "file": "code/solutions/11_1_quiet_times.js", + "number": 1, + "type": "js", + "code": "async function activityTable(day) {\n let logFileList = await textFile(\"camera_logs.txt\");\n // Your code here\n}\n\nactivityTable(1)\n .then(table => console.log(activityGraph(table)));", + "solution": "async function activityTable(day) {\n let table = [];\n for (let i = 0; i < 24; i++) table[i] = 0;\n\n let logFileList = await textFile(\"camera_logs.txt\");\n for (let filename of logFileList.split(\"\\n\")) {\n let log = await textFile(filename);\n for (let timestamp of log.split(\"\\n\")) {\n let date = new Date(Number(timestamp));\n if (date.getDay() == day) {\n table[date.getHours()]++;\n }\n }\n }\n\n return table;\n}\n\nactivityTable(1)\n .then(table => console.log(activityGraph(table)));" + }, + { + "name": "Real Promises", + "file": "code/solutions/11_2_real_promises.js", + "number": 2, + "type": "js", + "code": "function activityTable(day) {\n // Your code here\n}\n\nactivityTable(6)\n .then(table => console.log(activityGraph(table)));", + "solution": "function activityTable(day) {\n let table = [];\n for (let i = 0; i < 24; i++) table[i] = 0;\n\n return textFile(\"camera_logs.txt\").then(files => {\n return Promise.all(files.split(\"\\n\").map(name => {\n return textFile(name).then(log => {\n for (let timestamp of log.split(\"\\n\")) {\n let date = new Date(Number(timestamp));\n if (date.getDay() == day) {\n table[date.getHours()]++;\n }\n }\n });\n }));\n }).then(() => table);\n}\n\nactivityTable(6)\n .then(table => console.log(activityGraph(table)));" + }, + { + "name": "Building Promise.all", + "file": "code/solutions/11_3_building_promiseall.js", + "number": 3, + "type": "js", + "code": "function Promise_all(promises) {\n return new Promise((resolve, reject) => {\n // Your code here.\n });\n}\n\n// Test code.\nPromise_all([]).then(array => {\n console.log(\"This should be []:\", array);\n});\nfunction soon(val) {\n return new Promise(resolve => {\n setTimeout(() => resolve(val), Math.random() * 500);\n });\n}\nPromise_all([soon(1), soon(2), soon(3)]).then(array => {\n console.log(\"This should be [1, 2, 3]:\", array);\n});\nPromise_all([soon(1), Promise.reject(\"X\"), soon(3)])\n .then(array => {\n console.log(\"We should not get here\");\n })\n .catch(error => {\n if (error != \"X\") {\n console.log(\"Unexpected failure:\", error);\n }\n });", + "solution": "function Promise_all(promises) {\n return new Promise((resolve, reject) => {\n let results = [];\n let pending = promises.length;\n for (let i = 0; i < promises.length; i++) {\n promises[i].then(result => {\n results[i] = result;\n pending--;\n if (pending == 0) resolve(results);\n }).catch(reject);\n }\n if (promises.length == 0) resolve(results);\n });\n}\n\n// Test code.\nPromise_all([]).then(array => {\n console.log(\"This should be []:\", array);\n});\nfunction soon(val) {\n return new Promise(resolve => {\n setTimeout(() => resolve(val), Math.random() * 500);\n });\n}\nPromise_all([soon(1), soon(2), soon(3)]).then(array => {\n console.log(\"This should be [1, 2, 3]:\", array);\n});\nPromise_all([soon(1), Promise.reject(\"X\"), soon(3)]).then(array => {\n console.log(\"We should not get here\");\n}).catch(error => {\n if (error != \"X\") {\n console.log(\"Unexpected failure:\", error);\n }\n});" + }, + { + "name": "Tracking the scalpel [3rd ed]", + "file": "code/solutions/11_1_tracking_the_scalpel.js", + "number": "1[3]", + "type": "js", + "code": "async function locateScalpel(nest) {\n // Your code here.\n}\n\nfunction locateScalpel2(nest) {\n // Your code here.\n}\n\nlocateScalpel(bigOak).then(console.log);\n// → Butcher Shop", + "solution": "async function locateScalpel(nest) {\n let current = nest.name;\n for (;;) {\n let next = await anyStorage(nest, current, \"scalpel\");\n if (next == current) return current;\n current = next;\n }\n}\n\nfunction locateScalpel2(nest) {\n function loop(current) {\n return anyStorage(nest, current, \"scalpel\").then(next => {\n if (next == current) return current;\n else return loop(next);\n });\n }\n return loop(nest.name);\n}\n\nlocateScalpel(bigOak).then(console.log);\n// → Butcher's Shop\nlocateScalpel2(bigOak).then(console.log);\n// → Butcher's Shop", + "goto": "https://eloquentjavascript.net/3rd_edition/code/#11.1" + } + ], + "include": [ + "code/hangar2.js", + "code/chapter/11_async.js" + ], + "links": [ + "code/chapter/11_async.zip" + ] + }, + { + "number": 12, + "id": "12_language", + "title": "Project: A Programming Language", + "start_code": "run(`\ndo(define(plusOne, fun(a, +(a, 1))),\n print(plusOne(10)))\n`);\n\nrun(`\ndo(define(pow, fun(base, exp,\n if(==(exp, 0),\n 1,\n *(base, pow(base, -(exp, 1)))))),\n print(pow(2, 10)))\n`);\n", + "exercises": [ + { + "name": "Arrays", + "file": "code/solutions/12_1_arrays.js", + "number": 1, + "type": "js", + "code": "// Modify these definitions...\n\ntopScope.array = \"...\";\n\ntopScope.length = \"...\";\n\ntopScope.element = \"...\";\n\nrun(`\ndo(define(sum, fun(array,\n do(define(i, 0),\n define(sum, 0),\n while(<(i, length(array)),\n do(define(sum, +(sum, element(array, i))),\n define(i, +(i, 1)))),\n sum))),\n print(sum(array(1, 2, 3))))\n`);\n// → 6", + "solution": "topScope.array = (...values) => values;\n\ntopScope.length = array => array.length;\n\ntopScope.element = (array, i) => array[i];\n\nrun(`\ndo(define(sum, fun(array,\n do(define(i, 0),\n define(sum, 0),\n while(<(i, length(array)),\n do(define(sum, +(sum, element(array, i))),\n define(i, +(i, 1)))),\n sum))),\n print(sum(array(1, 2, 3))))\n`);\n// → 6" + }, + { + "name": "Comments", + "file": "code/solutions/12_3_comments.js", + "number": 3, + "type": "js", + "code": "// This is the old skipSpace. Modify it...\nfunction skipSpace(string) {\n let first = string.search(/\\S/);\n if (first == -1) return \"\";\n return string.slice(first);\n}\n\nconsole.log(parse(\"# hello\\nx\"));\n// → {type: \"word\", name: \"x\"}\n\nconsole.log(parse(\"a # one\\n # two\\n()\"));\n// → {type: \"apply\",\n// operator: {type: \"word\", name: \"a\"},\n// args: []}", + "solution": "function skipSpace(string) {\n let skippable = string.match(/^(\\s|#.*)*/);\n return string.slice(skippable[0].length);\n}\n\nconsole.log(parse(\"# hello\\nx\"));\n// → {type: \"word\", name: \"x\"}\n\nconsole.log(parse(\"a # one\\n # two\\n()\"));\n// → {type: \"apply\",\n// operator: {type: \"word\", name: \"a\"},\n// args: []}" + }, + { + "name": "Fixing scope", + "file": "code/solutions/12_4_fixing_scope.js", + "number": 4, + "type": "js", + "code": "specialForms.set = (args, scope) => {\n // Your code here.\n};\n\nrun(`\ndo(define(x, 4),\n define(setx, fun(val, set(x, val))),\n setx(50),\n print(x))\n`);\n// → 50\nrun(`set(quux, true)`);\n// → Some kind of ReferenceError", + "solution": "specialForms.set = (args, env) => {\n if (args.length != 2 || args[0].type != \"word\") {\n throw new SyntaxError(\"Bad use of set\");\n }\n let varName = args[0].name;\n let value = evaluate(args[1], env);\n\n for (let scope = env; scope; scope = Object.getPrototypeOf(scope)) {\n if (Object.hasOwn(scope, varName)) {\n scope[varName] = value;\n return value;\n }\n }\n throw new ReferenceError(`Setting undefined variable ${varName}`);\n};\n\nrun(`\ndo(define(x, 4),\n define(setx, fun(val, set(x, val))),\n setx(50),\n print(x))\n`);\n// → 50\nrun(`set(quux, true)`);\n// → Some kind of ReferenceError" + } + ], + "include": [ + "code/chapter/12_language.js" + ], + "links": [ + "code/chapter/12_language.zip" + ] + }, + { + "number": 13, + "id": "13_browser", + "title": "JavaScript and the Browser", + "start_code": "", + "exercises": [], + "include": null + }, + { + "number": 14, + "id": "14_dom", + "title": "The Document Object Model", + "start_code": "\n\n

\n \n

\n\n", + "exercises": [ + { + "name": "Build a table", + "file": "code/solutions/14_1_build_a_table.html", + "number": 1, + "type": "html", + "code": "\n\n

Mountains

\n\n
\n\n", + "solution": "\n\n\n\n

Mountains

\n\n
\n\n" + }, + { + "name": "Elements by tag name", + "file": "code/solutions/14_2_elements_by_tag_name.html", + "number": 2, + "type": "html", + "code": "\n\n

Heading with a span element.

\n

A paragraph with one, two\n spans.

\n\n", + "solution": "\n\n

Heading with a span element.

\n

A paragraph with one, two\n spans.

\n\n" + }, + { + "name": "The cat's hat", + "file": "code/solutions/14_3_the_cats_hat.html", + "number": 3, + "type": "html", + "code": "\n\n\n\n\n\n", + "solution": "\n\n\n\n\n\n\n\n\n\n" + } + ], + "include": null + }, + { + "number": 15, + "id": "15_event", + "title": "Handling Events", + "start_code": "\n\n

Drag the bar to change its width:

\n
\n
\n\n", + "exercises": [ + { + "name": "Balloon", + "file": "code/solutions/15_1_balloon.html", + "number": 1, + "type": "html", + "code": "\n\n

🎈

\n\n", + "solution": "\n\n

🎈

\n\n" + }, + { + "name": "Mouse trail", + "file": "code/solutions/15_2_mouse_trail.html", + "number": 2, + "type": "html", + "code": "\n\n\n\n", + "solution": "\n\n\n\n\n\n" + }, + { + "name": "Tabs", + "file": "code/solutions/15_3_tabs.html", + "number": 3, + "type": "html", + "code": "\n\n\n
Tab one
\n
Tab two
\n
Tab three
\n
\n", + "solution": "\n\n\n
Tab one
\n
Tab two
\n
Tab three
\n
\n" + } + ], + "include": null + }, + { + "number": 16, + "id": "16_game", + "title": "Project: A Platform Game", + "start_code": "\n\n\n\n\n\n\n\n \n\n", + "exercises": [ + { + "name": "Game over", + "file": "code/solutions/16_1_game_over.html", + "number": 1, + "type": "html", + "code": "\n\n\n\n\n\n\n\n\n", + "solution": "\n\n\n\n\n\n\n\n\n" + }, + { + "name": "Pausing the game", + "file": "code/solutions/16_2_pausing_the_game.html", + "number": 2, + "type": "html", + "code": "\n\n\n\n\n\n\n\n\n", + "solution": "\n\n\n\n\n\n\n\n\n" + }, + { + "name": "A monster", + "file": "code/solutions/16_3_a_monster.html", + "number": 3, + "type": "html", + "code": "\n\n\n\n\n\n\n\n\n \n", + "solution": "\n\n\n\n\n\n\n\n\n\n \n" + } + ], + "include": [ + "code/chapter/16_game.js", + "code/levels.js", + "code/_stop_keys.js" + ], + "links": [ + "code/chapter/16_game.zip" + ] + }, + { + "number": 17, + "id": "17_canvas", + "title": "Drawing on Canvas", + "start_code": "\n\n\n\n\n\n\n \n\n", + "exercises": [ + { + "name": "Shapes", + "file": "code/solutions/17_1_shapes.html", + "number": 1, + "type": "html", + "code": "\n\n\n\n\n\n\n", + "solution": "\n\n\n\n\n\n\n" + }, + { + "name": "The pie chart", + "file": "code/solutions/17_2_the_pie_chart.html", + "number": 2, + "type": "html", + "code": "\n\n\n\n\n\n\n", + "solution": "\n\n\n\n\n\n\n" + }, + { + "name": "A bouncing ball", + "file": "code/solutions/17_3_a_bouncing_ball.html", + "number": 3, + "type": "html", + "code": "\n\n\n\n\n\n\n", + "solution": "\n\n\n\n\n\n\n" + } + ], + "include": [ + "code/chapter/16_game.js", + "code/levels.js", + "code/_stop_keys.js", + "code/chapter/17_canvas.js" + ], + "links": [ + "code/chapter/17_canvas.zip" + ] + }, + { + "number": 18, + "id": "18_http", + "title": "HTTP and Forms", + "start_code": "\n\nNotes:
\n\n\n\n", + "exercises": [ + { + "name": "Content negotiation", + "file": "code/solutions/18_1_content_negotiation.js", + "number": 1, + "type": "js", + "code": "// Your code here.", + "solution": "const url = \"https://eloquentjavascript.net/author\";\nconst types = [\"text/plain\",\n \"text/html\",\n \"application/json\",\n \"application/rainbows+unicorns\"];\n\nasync function showTypes() {\n for (let type of types) {\n let resp = await fetch(url, {headers: {accept: type}});\n console.log(`${type}: ${await resp.text()}\\n`);\n }\n}\n\nshowTypes();" + }, + { + "name": "A JavaScript workbench", + "file": "code/solutions/18_2_a_javascript_workbench.html", + "number": 2, + "type": "html", + "code": "\n\n\n\n
\n\n",
+        "solution": "\n\n\n\n
\n\n"
+      },
+      {
+        "name": "Conway's Game of Life",
+        "file": "code/solutions/18_3_conways_game_of_life.html",
+        "number": 3,
+        "type": "html",
+        "code": "\n\n
\n\n\n", + "solution": "\n\n
\n\n\n\n" + } + ], + "include": null + }, + { + "number": 19, + "id": "19_paint", + "title": "Project: A Pixel Art Editor", + "start_code": "\n\n\n
\n\n", + "exercises": [ + { + "name": "Keyboard bindings", + "file": "code/solutions/19_1_keyboard_bindings.html", + "number": 1, + "type": "html", + "code": "\n\n\n
\n", + "solution": "\n\n\n
\n" + }, + { + "name": "Efficient drawing", + "file": "code/solutions/19_2_efficient_drawing.html", + "number": 2, + "type": "html", + "code": "\n\n\n
\n", + "solution": "\n\n\n
\n" + }, + { + "name": "Circles", + "file": "code/solutions/19_3_circles.html", + "number": 3, + "type": "html", + "code": "\n\n\n
\n", + "solution": "\n\n\n
\n" + }, + { + "name": "Proper lines", + "file": "code/solutions/19_4_proper_lines.html", + "number": 4, + "type": "html", + "code": "\n\n\n
\n", + "solution": "\n\n\n
\n" + } + ], + "include": [ + "code/chapter/19_paint.js" + ], + "links": [ + "code/chapter/19_paint.zip" + ] + }, + { + "number": 20, + "id": "20_node", + "title": "Node.js", + "start_code": "", + "exercises": [ + { + "name": "Search tool", + "file": "code/solutions/20_1_search_tool.mjs", + "number": 1, + "type": "js", + "code": "// Node exercises can not be ran in the browser,\n// but you can look at their solution here.\n", + "solution": "import {statSync, readdirSync, readFileSync} from \"node:fs\";\n\nlet searchTerm = new RegExp(process.argv[2]);\n\nfor (let arg of process.argv.slice(3)) {\n search(arg);\n}\n\nfunction search(file) {\n let stats = statSync(file);\n if (stats.isDirectory()) {\n for (let f of readdirSync(file)) {\n search(file + \"/\" + f);\n }\n } else if (searchTerm.test(readFileSync(file, \"utf8\"))) {\n console.log(file);\n }\n}\n" + }, + { + "name": "Directory creation", + "file": "code/solutions/20_2_directory_creation.mjs", + "number": 2, + "type": "js", + "code": "// Node exercises can not be ran in the browser,\n// but you can look at their solution here.\n", + "solution": "// This code won't work on its own, but is also included in the\n// code/file_server.js file, which defines the whole system.\n\nimport {mkdir} from \"node:fs/promises\";\n\nmethods.MKCOL = async function(request) {\n let path = urlPath(request.url);\n let stats;\n try {\n stats = await stat(path);\n } catch (error) {\n if (error.code != \"ENOENT\") throw error;\n await mkdir(path);\n return {status: 204};\n }\n if (stats.isDirectory()) return {status: 204};\n else return {status: 400, body: \"Not a directory\"};\n};\n" + }, + { + "name": "A public space on the web", + "file": "code/solutions/20_3_a_public_space_on_the_web.zip", + "number": 3, + "type": "js", + "code": "// Node exercises can not be ran in the browser,\n// but you can look at their solution here.\n", + "solution": "// This solutions consists of multiple files. Download it\n// though the link below.\n" + } + ], + "include": null, + "links": [ + "code/file_server.mjs" + ] + }, + { + "number": 21, + "id": "21_skillsharing", + "title": "Project: Skill-Sharing Website", + "start_code": "", + "exercises": [ + { + "name": "Disk persistence", + "file": "code/solutions/21_1_disk_persistence.mjs", + "number": 1, + "type": "js", + "code": "// Node exercises can not be ran in the browser,\n// but you can look at their solution here.\n", + "solution": "// This isn't a stand-alone file, only a redefinition of a few\n// fragments from skillsharing/skillsharing_server.js\n\nimport {readFileSync, writeFile} from \"node:fs\";\n\nconst fileName = \"./talks.json\";\n\nSkillShareServer.prototype.updated = function() {\n this.version++;\n let response = this.talkResponse();\n this.waiting.forEach(resolve => resolve(response));\n this.waiting = [];\n\n writeFile(fileName, JSON.stringify(this.talks), e => {\n if (e) throw e;\n });\n};\n\nfunction loadTalks() {\n try {\n return JSON.parse(readFileSync(fileName, \"utf8\"));\n } catch (e) {\n return {};\n }\n}\n\n// The line that starts the server must be changed to\nnew SkillShareServer(loadTalks()).start(8000);\n" + }, + { + "name": "Comment field resets", + "file": "code/solutions/21_2_comment_field_resets.mjs", + "number": 2, + "type": "js", + "code": "// Node exercises can not be ran in the browser,\n// but you can look at their solution here.\n", + "solution": "// This isn't a stand-alone file, only a redefinition of the main\n// component from skillsharing/public/skillsharing_client.js\n\nclass Talk {\n constructor(talk, dispatch) {\n this.comments = elt(\"div\");\n this.dom = elt(\n \"section\", {className: \"talk\"},\n elt(\"h2\", null, talk.title, \" \", elt(\"button\", {\n type: \"button\",\n onclick: () => dispatch({type: \"deleteTalk\",\n talk: talk.title})\n }, \"Delete\")),\n elt(\"div\", null, \"by \",\n elt(\"strong\", null, talk.presenter)),\n elt(\"p\", null, talk.summary),\n this.comments,\n elt(\"form\", {\n onsubmit(event) {\n event.preventDefault();\n let form = event.target;\n dispatch({type: \"newComment\",\n talk: talk.title,\n message: form.elements.comment.value});\n form.reset();\n }\n }, elt(\"input\", {type: \"text\", name: \"comment\"}), \" \",\n elt(\"button\", {type: \"submit\"}, \"Add comment\")));\n this.syncState(talk);\n }\n\n syncState(talk) {\n this.talk = talk;\n this.comments.textContent = \"\";\n for (let comment of talk.comments) {\n this.comments.appendChild(renderComment(comment));\n }\n }\n}\n\nclass SkillShareApp {\n constructor(state, dispatch) {\n this.dispatch = dispatch;\n this.talkDOM = elt(\"div\", {className: \"talks\"});\n this.talkMap = Object.create(null);\n this.dom = elt(\"div\", null,\n renderUserField(state.user, dispatch),\n this.talkDOM,\n renderTalkForm(dispatch));\n this.syncState(state);\n }\n\n syncState(state) {\n if (state.talks == this.talks) return;\n this.talks = state.talks;\n\n for (let talk of state.talks) {\n let found = this.talkMap[talk.title];\n if (found && found.talk.presenter == talk.presenter &&\n found.talk.summary == talk.summary) {\n found.syncState(talk);\n } else {\n if (found) found.dom.remove();\n found = new Talk(talk, this.dispatch);\n this.talkMap[talk.title] = found;\n this.talkDOM.appendChild(found.dom);\n }\n }\n for (let title of Object.keys(this.talkMap)) {\n if (!state.talks.some(talk => talk.title == title)) {\n this.talkMap[title].dom.remove();\n delete this.talkMap[title];\n }\n }\n }\n}\n" + } + ], + "include": null, + "links": [ + "code/skillsharing.zip" + ] + }, + { + "title": "JavaScript and Performance", + "number": 22, + "start_code": "\n\n\n", + "include": [ + "code/draw_layout.js", + "code/chapter/22_fast.js" + ], + "exercises": [ + { + "name": "Prime numbers", + "file": "code/solutions/22_1_prime_numbers.js", + "number": 1, + "type": "js", + "code": "function* primes() {\n for (let n = 2;; n++) {\n // ...\n }\n}\n\nfunction measurePrimes() {\n // ...\n}\n\nmeasurePrimes();\n", + "solution": "function* primes() {\n for (let n = 2;; n++) {\n let skip = false;\n for (let d = 2; d < n; d++) {\n if (n % d == 0) {\n skip = true;\n break;\n }\n }\n if (!skip) yield n;\n }\n}\n\nfunction measurePrimes() {\n let iter = primes(), t0 = Date.now();\n for (let i = 0; i < 10000; i++) {\n iter.next();\n }\n console.log(`Took ${Date.now() - t0}ms`);\n}\n\nmeasurePrimes();\n" + }, + { + "name": "Faster prime numbers", + "file": "code/solutions/22_2_faster_prime_numbers.js", + "number": 2, + "type": "js", + "code": "function* primes() {\n for (let n = 2;; n++) {\n // ...\n }\n}\n\nfunction measurePrimes() {\n // ...\n}\n\nmeasurePrimes();\n", + "solution": "function* primes() {\n let found = [];\n for (let n = 2;; n++) {\n let skip = false, root = Math.sqrt(n);\n for (let prev of found) {\n if (prev > root) {\n break;\n } else if (n % prev == 0) {\n skip = true;\n break;\n }\n }\n if (!skip) {\n found.push(n);\n yield n;\n }\n }\n}\n\nfunction measurePrimes() {\n let iter = primes(), t0 = Date.now();\n for (let i = 0; i < 10000; i++) {\n iter.next();\n }\n console.log(`Took ${Date.now() - t0}ms`);\n}\n\nmeasurePrimes();\n" + }, + { + "name": "Pathfinding [3rd ed]", + "file": "code/solutions/22_1_pathfinding.js", + "number": "1[3]", + "type": "js", + "code": "function findPath(a, b) {\n // Your code here...\n}\n\nlet graph = treeGraph(4, 4);\nlet root = graph[0], leaf = graph[graph.length - 1];\nconsole.log(findPath(root, leaf).length);\n// → 4\n\nleaf.connect(root);\nconsole.log(findPath(root, leaf).length);\n// → 2\n", + "solution": "function findPath(a, b) {\n let work = [[a]];\n for (let path of work) {\n let end = path[path.length - 1];\n if (end == b) return path;\n for (let next of end.edges) {\n if (!work.some(path => path[path.length - 1] == next)) {\n work.push(path.concat([next]));\n }\n }\n }\n}\n\nlet graph = treeGraph(4, 4);\nlet root = graph[0], leaf = graph[graph.length - 1];\nconsole.log(findPath(root, leaf).length);\n// → 4\n\nleaf.connect(root);\nconsole.log(findPath(root, leaf).length);\n// → 2\n", + "goto": "https://eloquentjavascript.net/3rd_edition/code/#22.1" + }, + { + "name": "Timing [3rd ed]", + "file": "code/solutions/22_2_timing.js", + "number": "2[3]", + "type": "js", + "code": "", + "solution": "function findPath(a, b) {\n let work = [[a]];\n for (let path of work) {\n let end = path[path.length - 1];\n if (end == b) return path;\n for (let next of end.edges) {\n if (!work.some(path => path[path.length - 1] == next)) {\n work.push(path.concat([next]));\n }\n }\n }\n}\n\nfunction time(findPath) {\n let graph = treeGraph(6, 6);\n let startTime = Date.now();\n let result = findPath(graph[0], graph[graph.length - 1]);\n console.log(`Path with length ${result.length} found in ${Date.now() - startTime}ms`);\n}\ntime(findPath);\n", + "goto": "https://eloquentjavascript.net/3rd_edition/code/#22.2" + }, + { + "name": "Optimizing [3rd ed]", + "file": "code/solutions/22_3_optimizing.js", + "number": "3[3]", + "type": "js", + "code": "", + "solution": "function time(findPath) {\n let graph = treeGraph(6, 6);\n let startTime = Date.now();\n let result = findPath(graph[0], graph[graph.length - 1]);\n console.log(`Path with length ${result.length} found in ${Date.now() - startTime}ms`);\n}\n\nfunction findPath_set(a, b) {\n let work = [[a]];\n let reached = new Set([a]);\n for (let path of work) {\n let end = path[path.length - 1];\n if (end == b) return path;\n for (let next of end.edges) {\n if (!reached.has(next)) {\n reached.add(next);\n work.push(path.concat([next]));\n }\n }\n }\n}\n\ntime(findPath_set);\n\nfunction pathToArray(path) {\n let result = [];\n for (; path; path = path.via) result.unshift(path.at);\n return result;\n}\n\nfunction findPath_list(a, b) {\n let work = [{at: a, via: null}];\n let reached = new Set([a]);\n for (let path of work) {\n if (path.at == b) return pathToArray(path);\n for (let next of path.at.edges) {\n if (!reached.has(next)) {\n reached.add(next);\n work.push({at: next, via: path});\n }\n }\n }\n}\n\ntime(findPath_list);\n", + "goto": "https://eloquentjavascript.net/3rd_edition/code/#22.3" + } + ] + } +]; diff --git a/html/code/draw_layout.js b/html/code/draw_layout.js new file mode 100644 index 000000000..0c1a87c56 --- /dev/null +++ b/html/code/draw_layout.js @@ -0,0 +1,104 @@ +// The familiar Vec type. + +class Vec { + constructor(x, y) { + this.x = x; this.y = y; + } + plus(other) { + return new Vec(this.x + other.x, this.y + other.y); + } + minus(other) { + return new Vec(this.x - other.x, this.y - other.y); + } + times(factor) { + return new Vec(this.x * factor, this.y * factor); + } + get length() { + return Math.sqrt(this.x * this.x + this.y * this.y); + } +} + +// Since we will want to inspect the layouts our code produces, let's +// first write code to draw a graph onto a canvas. Since we don't know +// in advance how big the graph is, the `Scale` object computes a +// scale and offset so that all nodes fit onto the given canvas. + +const nodeSize = 6; + +function drawGraph(graph, layout) { + let parent = (window.__sandbox ? window.__sandbox.output.div : document.body); + let canvas = parent.querySelector("canvas"); + if (!canvas) { + canvas = parent.appendChild(document.createElement("canvas")); + canvas.width = canvas.height = 400; + } + let cx = canvas.getContext("2d"); + + cx.clearRect(0, 0, canvas.width, canvas.height); + let scale = new Scale(layout, canvas.width, canvas.height); + + // Draw the edges. + cx.strokeStyle = "orange"; + cx.lineWidth = 3; + for (let i = 0; i < layout.length; i++) { + let conn = graph.neighbors(i); + for (let target of conn) { + if (conn <= i) continue; + cx.beginPath(); + cx.moveTo(scale.x(layout[i].x), scale.y(layout[i].y)); + cx.lineTo(scale.x(layout[target].x), scale.y(layout[target].y)); + cx.stroke(); + } + } + + // Draw the nodes. + cx.fillStyle = "purple"; + for (let pos of layout) { + cx.beginPath(); + cx.arc(scale.x(pos.x), scale.y(pos.y), nodeSize, 0, 7); + cx.fill(); + } +} + +// The function starts by drawing the edges, so that they appear +// behind the nodes. Since the nodes on _both_ side of an edge refer +// to each other, and we don't want to draw every edge twice, edges +// are only drawn then the target comes _after_ the current node in +// the `graph` array. + +// When the edges have been drawn, the nodes are drawn on top of them +// as purple discs. Remember that the last argument to `arc` gives the +// rotation, and we have to pass something bigger than 2π to get a +// full circle. + +// Finding a scale at which to draw the graph is done by finding the +// top left and bottom right corners of the area taken up by the +// nodes. The offset at which nodes are drawn is based on the top left +// corner, and the scale is based on the size of the canvas divided by +// the distance between those corners. The function reserves space +// along the sides of the canvas based on the `nodeSize` variable, so +// that the circles drawn around nodes’ center points don't get cut off. + +class Scale { + constructor(layout, width, height) { + let xs = layout.map(node => node.x); + let ys = layout.map(node => node.y); + let minX = Math.min(...xs); + let minY = Math.min(...ys); + let maxX = Math.max(...xs); + let maxY = Math.max(...ys); + + this.offsetX = minX; this.offsetY = minY; + this.scaleX = (width - 2 * nodeSize) / (maxX - minX); + this.scaleY = (height - 2 * nodeSize) / (maxY - minY); + } + + // The `x` and `y` methods convert from graph coordinates into + // canvas coordinates. + x(x) { + return this.scaleX * (x - this.offsetX) + nodeSize; + } + y(y) { + return this.scaleY * (y - this.offsetY) + nodeSize; + } +} diff --git a/html/code/file_server.mjs b/html/code/file_server.mjs new file mode 100644 index 000000000..45b53e0d5 --- /dev/null +++ b/html/code/file_server.mjs @@ -0,0 +1,106 @@ +import {createServer} from "node:http"; + +const methods = Object.create(null); + +createServer((request, response) => { + let handler = methods[request.method] || notAllowed; + handler(request).catch(error => { + if (error.status != null) return error; + return {body: String(error), status: 500}; + }).then(({body, status = 200, type = "text/plain"}) => { + response.writeHead(status, {"Content-Type": type}); + if (body?.pipe) body.pipe(response); + else response.end(body); + }); +}).listen(8000); + +async function notAllowed(request) { + return { + status: 405, + body: `Method ${request.method} not allowed.` + }; +} + +import {resolve, sep} from "node:path"; + +const baseDirectory = process.cwd(); + +function urlPath(url) { + let {pathname} = new URL(url, "http://d"); + let path = resolve(decodeURIComponent(pathname).slice(1)); + if (path != baseDirectory && + !path.startsWith(baseDirectory + sep)) { + throw {status: 403, body: "Forbidden"}; + } + return path; +} + +import {createReadStream} from "node:fs"; +import {stat, readdir} from "node:fs/promises"; +import {lookup} from "mime-types"; + +methods.GET = async function(request) { + let path = urlPath(request.url); + let stats; + try { + stats = await stat(path); + } catch (error) { + if (error.code != "ENOENT") throw error; + else return {status: 404, body: "File not found"}; + } + if (stats.isDirectory()) { + return {body: (await readdir(path)).join("\n")}; + } else { + return {body: createReadStream(path), + type: lookup(path)}; + } +}; + +import {rmdir, unlink} from "node:fs/promises"; + +methods.DELETE = async function(request) { + let path = urlPath(request.url); + let stats; + try { + stats = await stat(path); + } catch (error) { + if (error.code != "ENOENT") throw error; + else return {status: 204}; + } + if (stats.isDirectory()) await rmdir(path); + else await unlink(path); + return {status: 204}; +}; + +import {createWriteStream} from "node:fs"; + +function pipeStream(from, to) { + return new Promise((resolve, reject) => { + from.on("error", reject); + to.on("error", reject); + to.on("finish", resolve); + from.pipe(to); + }); +} + +methods.PUT = async function(request) { + let path = urlPath(request.url); + await pipeStream(request, createWriteStream(path)); + return {status: 204}; +}; + +import {mkdir} from "node:fs/promises"; + +methods.MKCOL = async function(request) { + let path = urlPath(request.url); + let stats; + try { + stats = await stat(path); + } catch (error) { + if (error.code != "ENOENT") throw error; + await mkdir(path); + return {status: 204}; + } + if (stats.isDirectory()) return {status: 204}; + else return {status: 400, body: "Not a directory"}; +}; diff --git a/html/code/hangar2.js b/html/code/hangar2.js new file mode 100644 index 000000000..19329d11b --- /dev/null +++ b/html/code/hangar2.js @@ -0,0 +1,350 @@ +var readTextFile = function() { + let min = 60 * 1000, hour = min * 60, day = hour * 24 + + let logs = [], year = 2023, start = new Date(year, 8, 21).getTime() + for (let i = 21; i <= 30; i++) { + if (i != 27) logs.push({name: "activity-" + year + "-09-" + i + ".log", time: start + i * day, day: (i - 17) % 7}) + } + + let rState = 81782 + function r1() { + rState ^= rState << 13 + rState ^= rState << 17 + rState ^= rState << 5 + return (rState & 0xffffff) / 0xffffff + } + function r(n) { + return Math.floor(r1() * n) + } + + let weekday = [1, 1, 1, 1, 1, 3, 8, 20, 10, 15, 15, 20, 25, 12, 15, 20, 18, 16, 10, 8, 8, 7, 4, 2] + let saturday = [1, 1, 1, 1, 1, 2, 3, 5, 3, 2, 2, 5, 8, 7, 9, 5, 5, 3, 3, 3, 4, 2, 2, 2] + let sunday = [2, 2, 1, 1, 1, 1, 1, 2, 2, 3, 6, 6, 2, 1, 1, 1, 1, 4, 4, 4, 3, 2, 1, 1] + + let activity = (day, base) => { + let schedule = day == 0 ? sunday : day == 6 ? saturday : weekday + let events = [] + for (let h = 0; h < 24; h++) { + let n = schedule[h] * 2 + r(5) - 2 + for (let i = 0; i < n; i++) { + let t = base + h * hour + r(hour) + let j = events.length + while (j > 0 && events[j - 1] > t) --j + events.splice(j, 0, t) + } + } + return events + } + + let generated = false + function generateLogs() { + if (generated) return + generated = true + for (let log of logs) { + files[log.name] = activity(log.day, log.time).join("\n") + } + } + + let files = { + __proto__: null, + "shopping_list.txt": "Peanut butter\nBananas", + "old_shopping_list.txt": "Peanut butter\nJelly", + "package.json": '{"name":"test project","author":"cāāw-krö","version":"1.1.2"}', + "plans.txt": "* Write a book\n * Figure out asynchronous chapter\n * Find an artist for the cover\n * Write the rest of the book\n\n* Don't be sad\n * Sit under tree\n * Study bugs\n", + "camera_logs.txt": logs.map(l => l.name).join("\n") + } + + return function readTextFile(filename, callback) { + if (/^activity/.test(filename)) generateLogs() + let file = filename == "files.list" ? Object.keys(files).join("\n") : files[filename] + Promise.resolve().then(() => { + if (file == null) callback(null, "File " + filename + " does not exist") + else callback(file) + }) + } +}() + +var activityGraph = table => { + let widest = Math.max(50, Math.max(...table)) + return table.map((n, i) => { + let width = (n / widest) * 20 + let full = Math.floor(width), rest = " ▏▎▍▌▋▊▉"[Math.floor((width - full) * 8)] + return String(i).padStart(2, " ") + " " + "█".repeat(full) + rest + }).join("\n") +} + +var joinWifi = function(networkID, code) { + return new Promise((accept, reject) => { + setTimeout(() => { + if (networkID != "HANGAR 2") return reject(new Error("Network not found")) + let correct = "555555" + if (code == correct) return accept(null) + if (!correct.startsWith(code)) return reject(new Error("Invalid passcode")) + }, 20) + }) +} + +var request = function(){ + function error(device) { + return (req, resolve, reject) => reject(new Error(device + ": malformed request")) + } + + let hosts = { + "10.0.0.1": error("ROUTER772"), + "10.0.0.2": () => {}, + "10.0.0.4": () => {}, + "10.0.0.20": error("Puxel 7"), + "10.0.0.33": error("jPhone[K]"), + } + + function screen(n) { + return (req, resolve, reject) => { + if (!req || req.command !== "display") { + reject(new Error("LedTec SIG-5030: INVALID REQUEST " + req?.type)) + } else if (!Array.isArray(req.data) || req.data.length !== 1500) { + reject(new Error("LedTec SIG-5030: INVALID DISPLAY DATA")) + } else { + if (!screens) { + if (typeof window != "object" || !window.document) return + screens = new Screens + } + setTimeout(() => { + screens.update(n, req.data) + resolve({status: "ok"}) + }, 3 + Math.floor(Math.random() * 20)) + } + } + } + + ;["10.0.0.44", "10.0.0.45", "10.0.0.41", + "10.0.0.31", "10.0.0.40", "10.0.0.42", + "10.0.0.48", "10.0.0.47", "10.0.0.46"].forEach((addr, i) => hosts[addr] = screen(i)) + + class Screens { + constructor() { + this.getParent() + let doc = this.parent.ownerDocument + this.dom = this.parent.appendChild(doc.createElement("div")) + this.dom.style.cssText = "position: relative; max-width: 500px" + let inner = this.dom.appendChild(doc.createElement("div")) + inner.style.cssText = "position: relative; width: 100%; padding-bottom: 60%" + this.screens = [] + for (let i = 0; i < 9; i++) { + let screen = inner.appendChild(doc.createElement("div")) + let row = Math.floor(i / 3), col = i % 3 + screen.style.cssText = "border: 1px solid #222; background: black; position: absolute; width: 33.3%; height: 33.3%; left: " + (col * 33.3) + "%; top: " + (row * 33.3) + "%" + let canvas = screen.appendChild(doc.createElement("canvas")) + canvas.style.cssText = "width: 100%; height: 100%" + this.screens.push(canvas) + } + this.screens.forEach(c => { c.width = c.offsetWidth; c.height = c.offsetHeight }) + } + + getParent() { + this.parent = window.__sandbox ? window.__sandbox.output.div : document.body + } + + update(n, data) { + this.getParent() + if (!this.parent.ownerDocument.body.contains(this.dom)) this.parent.appendChild(this.dom) + + let canvas = this.screens[n], cx = canvas.getContext("2d") + cx.clearRect(0, 0, canvas.width, canvas.height) + let gapX = (canvas.width * 0.4) / 51, sizeX = (canvas.width * 0.6) / 50, skipX = gapX + sizeX + let gapY = (canvas.height * 0.4) / 31, sizeY = (canvas.height * 0.6) / 30, skipY = gapY + sizeY + for (let i = 0, col = 0, row = 0; i < 1500; i++) { + let pixel = data[i] + if (pixel) { + cx.fillStyle = pixel == 3 ? "#fd4" : pixel == 2 ? "#a82" : "#741" + cx.fillRect(gapX + col * skipX, gapY + row * skipY, sizeX, sizeY) + } + if (col == 49) { col = 0; row++ } + else { col++ } + } + } + } + let screens = null + + return function request(address, content) { + return new Promise((resolve, reject) => { + let host = hosts[address] + if (!host) reject(new Error("No route to host " + address)) + else host(JSON.parse(JSON.stringify(content)), resolve, reject) + }) + } +}() + +var clipImages = [ + [ + " 5dc", + " e9.2 2b.3 .3 2b.o.3o. 2b.o.3o. 27.2 2.o2.o2. 27.o. .Oo.oO. . 25.o. .Oo.oO.4 20.2 2.Oo .Oo.oO.2o. 20.o 2.Oo.2Oo.oO.2o. 20.O. .O2.2Oo2O2o3. 20.O2 2oOo.O2oO2o3. 20.oOo oO2oO2oO2oOo. 1e.o2O3o2O8oOo 1f.O6oOao 1e.oO11o 1eoO12o. 1c.O13o. 6. 14.O14o. 1boO15. 1bO15o. 1aoO15o 1a.O16. 18. .O15o 1boO15. 1a.O15o 1b.O15. f", + " 57e.2 5c", + " 12d.2 1f0. 30.O 2f.oO 2foO2 2eoO3 2d.O4 2c.oO4 2b.O5o 2b.oOo.2 2d.2 fe", + " b.oO15. 1aO16o 1b.oO14. 1a.oO14. 1boO15. 1b.O14o 1c.O14o 1b.O15. 1a.O16. 1a.O16. 1aoO16o.3 16.O1bo. 14oO1e. 11.O20o a.6oO22. 8O2b. 6O2c. 5O29o.3 5O25o2. aO23o. dO1fo.2 10O5o2O11o3.3 14o.4 2.5oO7o.3 27.3oO3 2fo2. 30. e7", + " 33c.2 b7.4 7a.2 31. 135", + " 370.3 31. 237", + " 4c1.2 119", + " 5dc" + ], + [ + " 5dc", + " 1e3.2 30.3 .3 2a.o2. .o. 26.3 .o2.2o2. 26.o. .Oo.2o2. 26.O. .Oo.oOo. 22. 3.Oo oOo.oO.5 1e.o2 2oOo.oOo2O2.5 1foO. .O2.oOo2O2.2o2. 1d. oOo .O2.oOo2Oo.o3. 1c.o3O2o.O3oO2oOo4. 1c.2O6oO8o2Oo 1c.oO13. 1b.oO13o. c.2 c.O15o. 1aoO16. 18.oO17. 6.3 f.O17o. 18oO17. 18oO17o 19oO16o. b", + " 5dc", + " 2edo 30.O 2f.O2 2e.O3 2d.oO3 2c.oO4 2coO3o. 2co3. 192", + " c.oO16o 19oO16o 1a.oO14o. 19.O15o. 1a.O14o. 1b.oO13o 1b.O15o 1b.O15. 1b.O15o 1boO17o. 17.O1bo 14.O1do. b.2o3.oO20o. 8.O29o. 6O2c. 5O2ao2. 5O26o2.2 8O24o. cO5o3Oo2O15o2.2 eOo.3 3. 2.o2O6o6.5 13. c.3O3o 2e.o2. 2b.2 181", + " 29f. 36. 81.2 30.4 7a.2 1d2", + " 304.4 6.2 2cc", + " 5dc", + " 5dc" + ], + [ + " 47a. 132.2 2d", + " 345.2 2.2 2c.o. .3 26. 3.o2.2o2. 26.2 2.o2.2o2. 25.o2 .oOo.o2. 22o2 2.O2.2O2o2Oo.3 20oO. .O2.oOo2O2o.3 1d.o.oOo .O2.oOo2O2o.2o. 1a.2oO5.oO2o2O2oO2o.o2 19.2oO8oO8o4. 18.O14oO2o. 17.O17o. 17oO18o. 16.oO18o. 6", + " 5dc", + " 2ba.o 30oO 2e.oO2 2eoO3 2d.O4 2c.O3o. 2coOo. 2e.2 1c6", + " f.O1ao 15.oO19o. 14.O1ao. 14.o2O17o.2 15oO16o2. 17.oO14o.2 19oO14o 1b.O15. 1a.oO17o. 16.oO1ao. f.4oO1eo. boO26. 9oO29o. 6O2bo. 5O29o3. 5O26o2. 9O2o4.2o.2oO18o. a.2o.2 9.2oO13o. 1doO2o. .d 1f.o.2 213", + " 265.2 3. b5.2 7a.2 23e", + " 29a. 31.2 7. 2e5. 20", + " 5dc", + " 5dc" + ], + [ + " 5dc", + " 5dc", + " 5a1.2 39", + " 1a1. 83. 30.o 2f.oO 2e.oO2 2c.oO2o. 2b.oOo.2 2b.2o.2 2e.2 25d", + " 28.3 2. 27.2 2.7 21.2 3.6o2.4 16.co. 2.o2.2oO2o3.2 e.o5O7o6Oo.2oOo.oO4o2.2 c.oO14o.oO2o2O6o.2 a.oO18oO2oO6o2.3 8.O24o3.2 5.2oO26o2. 3.oO2ao.2 3oO2ao. 5O2ao. 6O2ao. 6o5.2oO20o3. 6. 6oO1do.2 10.oO1ao2.2 12.oO17o2. 16.oO15. 1b.o6Oo2Oc. 1e.3o4Oco 21.o3Oo2O9o 22.o2O2oOa. . 20.o2Oco.3 20.2o5O2oO4o3. 22.5o.2o4O3o.2 24.3 .4oO3o. 2b.3o3. 2e.3 73", + " 5dc", + " 259.4 6.2 2f.4 344", + " 5dc", + " 5dc" + ], + [ + " 433. 1a8", + " 5dc", + " 598.3 41", + " 256.o 2f.oO 2d.o3. 2b.2o2. 2c.o2. 2f. 290", + " a3.5 29.o2O7o3.2 21.oO10o2. 1c.O15o.4 16.oO1ao. 13.O1eo. e.2oO21o c.oO24o. aO28o.7 2o2.o4O23o.4 3. 7.oO1do4.4 9.oO1do2.o4.3 9oO1bo6O3o3. 9oO18o. 3.o2O5o2. 9.oO14.3 6.2oO5o. 9oO15 a.2oO3o2. 9.oO14. a.2o3.3 9.O15o c.4 b.O16. 1b.oO14. 1c.oO13. 1d.o5Oe. 1f.2o3Odo 21oOfo 21oO4oOb. 20.3oOd. 22.oOdo 13", + " 5dc", + " 28b.5 34c", + " f.o2Oco 22.2oO2oO9o. 23.o2.oO2o2O2o3.3 21.3 .Oo.2o2.2o.2o. 20.2 2.o.5 .4o.2 23.3 2. 3.5 4bf", + " 443.3 196" + ], + [ + " 5dc", + " 48a. 151", + " 4b8.2 d6.2 31.2 17", + " 256.o 2e.o3 2c.2o.2 2c.3 2e.2 37. 28a", + " a2.6 29.oO9o.3 21.oO10o. 1d.O15o.3 17.O1bo. 12.oO1eo. e.oO22o c.O25o. aO28o. 8.6oO23o e.oO1fo2. e.oO1co. 11oO1c. 7. 6.4 2oO1bo 15oO1c. 13.o2O1bo 14oO1d. 13O1e. 13.oO1co 13.oO15oO2oO4. 12.O16o3.oO3o 12.oO15o. .2o4. 12.O15o 3.d coO14o 3.e b.O14o 9.4o2.2 coO13o 9.3o4. doO2oOfo b.6 3", + " 5dc", + " 3fe.2 1dc", + " a.o2.o3Oco c.3 10. oO10 20.O11. 1f.O11. 1f.o3Oeo 21.2oOdo 22oOeo. 20.Ofo. 21o3Oco. 21.2oO2o2O8o. 23oOo.oO2oO2oOo.2 23oO. oOo.O2o3.2 23o2 2oOo.oO.5 23.2 2oOo o2.8 24.o. .o. . 2.3 24.o. .2 6. 26.2 2. 2a1", + " 43b.3 19e" + ], + [ + " 5dc", + " 4b8.2 122", + " 4e6.3 d5.3 1b", + " 257. 2f.o2 2d.2o. 2c.4 2d.3 66.3 258", + " d3.2o5.3 26.oObo2.2 1f.O13o. 1boO16o3.2 15oO1co. 11oO21. c.o2O23o bO28. 9o6O24. d.oO23. e.O1eo.3 d.oO1co.2 11oO1bo 5.2 eoO1bo 15.O1c. 14oO1co 14.oO1bo 14oO1d 14o2O1c. 14.O1c. 13.O1do 14O1e. 13.O1do 14oO16oO5o. 13oO15o.oO5. 13.oO6oOd.2oO2oOo. b", + " 16. 4ca. 30.2 c8", + " 42d. 1ae", + " 9oO14 2.o2.o2.5 11oO12o 2.d f.O2o5Oc 2.2 .3 .7 e.o2.o3Od. 9.6 10.oO10. 9.6 10.O5oOb. 1f.O11o 1f.o3Of 21.2oOe. 21oOeo. 20.Ofo. 20.Ofo.2 1f.o3Oco. 21.2oO2.oO7o2. 22.oOo.O3oO2oOo2. 22.O2 .O2o.O2.o2.2 22.o2 .O2.2Oo.o2.2 22.o. .O2.2Oo.2o.2 23. 2.oO.2o2.4 28o2 2.o. .2 28.o 2.3 2b.2 3. 114.3 90", + " 469.3 170" + ], + [ + " 5dc", + " 53b.3 9e", + " 5dc", + " 225. 2e.oO2 2b.2o4. 2a.2o. 2e. ba.7 234", + " 104.5o3.3 25.oOeo2.2 1c.oO15o.4 14.oO1do. f.oO21o coO26. aO28o. 8.2o2O26o. b.oO25. b.O21o.3 c.O1eo.2 10.O1bo.2 13.O1b 16.O1b 16.O1b. 15.O1b. 16oO1a. 16oO1a. 16oO1a. 16.O1a. 17oO19. 17oO19o 17.O19o 17.O1a 17.O1a. e", + " 4.2 32. 4fa.5 a4", + " 5dc", + " 9oO19. 17oO19o 17oOo5O13o 17.o2O18 18.oO17o. 17.O5oO11o2. 17oO4o2O12. 18.O4oO13. 18.o3O14o. 19.2oO13o2. 19.oO11o2Oo2.2 18.O12o5.2 18oO3oOeo3.4 18.O3oOeo3.4 18.2oOo2Oco4.2 1b.oOo3Obo.5 1b.o2.2oOo2O7o.6 1boOo.2O2o2O7o.5 1b.o2.2oOo.oOo2Oo2Oo.3 1d.o2 .oOo.oOo2Oo2O. 20.o. .Oo.2O2o2Oo2O. 21. 2oOo.oOo.O2.oO. 23.oO.2oOo.O2.2o. 23.o2.2oO.2Oo.2o. 23.o2 .o2.2o2. .2 23.o. .o2.2o2. .2 24.2 .o2 .o2. 27. 2.o. .o2. 2a.3 2.3 2b.2 2.2 19", + " 48b.2 14f" + ], + [ + " 3c7.3 212", + " 563.2 77", + " 59f.2 3b", + " 255.2o 2c.o2O3 2boOo3.2 10b.6 2c.5 1de", + " 90.2 f.3 c3.2o5O5o9.2 19.oO17o3.2 12.oO1eo. d.2oO22. aoO27o 9O2ao. 6.3oO28o 8.2o2O24o2 coO20o. 10oO1co. 13.O19o. 16.O17o. 18.O17. 1aoO16. 1aoO16. 1aoO16 1boO15o 1boO15o 1boO15o 1boO15o 1boO15o 1boO15. 1boO14o. 12", + " 516. 44.3 7e", + " 494. d9. 6d", + " 9oO14o. 1boO13o.2 1boO12o.2 1coO12.3 1b.oO11o. 1e.oO10o. 1e.oO10.2 1e.oOoOdo. 1f.o3Odo. 1e.o4Oao3.2 1e.o.2oOao3. 1f.4o4O6o.4 1f.3o5O6o.4 20.2o5O5o.2 23.2o5O2oO2o. 24.4o3Oo2Oo.2 24.4o8. 25.4o7.2 26.4o3.5 27.a 29.6 2d.4 30.2 17f", + " 3bf. f2.2 128" + ], + [ + " 420.2 1ba", + " 5ba.3 1f", + " 8c.2 30.3 51b", + " 2e6. 3.2o2 2b.oO5 2b.oO2o2. 2d.2 9e.7 140.2 74", + " b6. 43.2 21.3 26.o6. 27.2oO7. 25.oOa. 1f.4o2Obo.3 18.o3O16o. 14.O1e. f.2oO20o a.2oO26. 7O2co. 4O2oO2ao 4. .oO26o.3 8.2o3O1eo. 12oO1ao. 15oO18o. 16.O16o. 19.Ofo4.2 1coOoO9o2.2 21.o3Oo2O2o2Oo2. 23.3oa.2 8.2 19.3o9.2 24.7o4.3 25.7o2.3 26.b 26.b 20", + " 1b.3 563.3 58", + " 595. 46", + " 8. .8 29.8 29.8 2a.8 29.7 2b.6 2b.7 2c.4 2f. 2b0.3 bf. d0", + " 5dc" + ], + [ + " 41a. 1c1", + " 5b2.4 26", + " b6.3 523", + " 124.3 1e9.2 c.2 2c.2o2O2 29. .oO2o2. 2c.2 68.3 2f.3 110. 7c", + " b4.2 2b.4o4. 27.2o2O6. 25.2o2O7o 23.3oO9o. 22.2oObo 21.o2Odo .2 1c.oO11oO3o. 17.oO19o. 13oO1e. f.oO21o. 9.2o2O25o. 5oO2c. 4O2ao.3 4.3oO23o.2 c.2oO1eo. 10.2oO1bo 12.2o9O11o. 13.3oOo3.2 2oOeo. 14.3o4.5 .oOo.3o3.4 17.2o4.4 5. 22.2o3.3 d. 1c.2o2.4 29.8 2b.6 2d.4 5d", + " 13.3 564. 31.2 2e", + " 4e5. da. 1b", + " 441.3 198", + " 5dc" + ], + [ + " 351. 31. 31. 30.2 30.2 30.2 30.2 30.2 30.2 30.2 31. 26.3 8. 31. 31.", + " 25f.3 2b.a 26.e 23.7o4.7 20.2ob.7 1e.o7O5o5.3 1d.o2Ofo2.2 1c.oO12o2. 1boO15o.2 19oO16o2. 18oO17o2. 17oO19o. 16oO1ao. 15o2O1b. 14oO1co. 13o2O1co 13.oO1d. 12.2oO1co 12", + " 3e. 59d", + " 265. 30.3 b7.2 2c.6 2b.5 228", + ".3o2O1b 12.4o3O18o 15.3oO18o 16.3oO17. 18.2O17. 1aoO16 1boO16 1boO15o 1boO16.4 17oO1bo. 14oO1do. 12oO1eo. 11.O20o. foO22o. a.oO24o. 7.2oO22o2.2 7o2O23o. b.2oO20o. 10.oO1co. 15.o2O16o. 1a.2oO10o2.2 1f.2oO8o2.3 24.oOo.5 2a.2 14c", + " 2e6.3 2f3", + " 5dc", + " 3c8.2 212", + " 575.2 31.2 32" + ], + [ + " 565. 31.2 43", + " ce. 2e.9 29.2o6.2 28.oO6o.2 27.oO7o.2 24.3oO8o.2 23.3o2O8o.2 22.2o3O9o2.3 20.o2Oco.3 1e.2o2Odo3. 1c.3o2O10o. 1b.2o2O11o. 1b.2o2O12o. 1b.2oO13o 1b.2oO14. 1a.2oO14o 1b.oO15. 1a.2oO14o. 1a.oO15o 1a.2oO15. 1a.2O15o 1b.oO15o 1boO16. 1a.O17 1a.O17. 1aoO16o 13", + " 5dc", + " 28e.3 30.2 bf.2 2e.oO2 2b.3o2.2 2b.3 94. 131", + " 7.O17. 19.O17. 19.O17. 19.O16o 1a.O16o 1a.O16o 1a.oO15. 1b.O15. 1boO15. 1boO15o.o3. 16oO1bo. 14oO1do 13.O1f. 12oO1fo. 10oO21o. c.oO21o2. 9.oO21o.2 8o3O21o2. boO21o. f.oO1do. 14.oO19.2 18.oO12o3. 1c.4oO9o2.2 26.O3o.3 2b.o. 119", + " 2dd.3 2fc", + " 5dc", + " 3c0.2 30.2 1e8", + " 59e.4 3a" + ], + [ + " 58b. 31.2 1d", + " 71.2 2d.6 1a. 10.9 29.5o2.3 26. 2.4o4.2 24.5o2.o6. 23.2o.2o4O4o. 24.o2.2o4O4o. 24.Oo.oO2oO5. 21.3 oOo.oO7o. 20.o2.2O2o2O7o.2 20oOo3Obo. 20.oOfo. 1f.O11. 1f.O11o. 1e.O12. 1e.oO11o. 1eO13. 1eoO12o 1eoO12o. 1dO14. 1doO13. 1d.O13o 1c.oO13o 1c.O14o 1c.O15. 1boO15o 1b.O15o. 14", + " 5dc", + " f2.3 321.oO2 2c.2oO3 2b.o3.2 2c.2 131", + " 7oO15. 1aoO16o 1aO17o 1a.O16o 14.2 4.O16o 1aoO16o 1aoO16o 1a.O16o 1a.O16o 1a.O16o 1b.O15o 1b.O15o.o4. 15.O1co. 13.O1eo. 12oO1f. 11.O21. 10oO21o. d.O20o3. a.2oO1eo2. 9. .2oO1fo. cO21o. fO1fo. 12oO1bo. 15.2oO15o.2 1a.oOo2Oco2. 24.4oO3o.2 2co3 b5", + " 5e.3 289.2 17.3 16.2 12b.3 190", + " 418. 31.2 1a.3 173", + " 5dc", + " 5c4.3 15" + ], + [ + " 583.3 56", + " 85.2 23.2 2d.2 .3 2b.7 2b.7 27.2 2.o.3o.4 23.4 .o.3o2.4 23.o. .Oo.oO2o2. 24.o.3O3oO2o2. 24.o2.2oO6o2 21.2 2oOo.oO7o. 20.o. .O2.2O7o. 20.oOo.oOo.oO6o.3 1f.O2o.O2o2O7.3 1e.2oOeo.3 1doO10o2.2 1d.O11o2. 1d.O11o2. 1doOoO10o. 1doO12o. 1d.O12o. 1c.o2O11o2. 1a.O2o2O11o. 1boO14o. 1boO15o. 19oO16o. 19oO16. 1aoO16. 13", + " 3b5. 226", + " 449o2O 2c.2oOo2 2b.o2.2 2c.2 100", + " 7oO16. 1a.O16. 19oO17. 19oO17. 1aoO16. 19.O17. 19oO17. 19.O17 1a.O16o 1a.O16o 1boO16 1boO16.2o3. 15oO1co. 13.O1eo. 12oO1f. 11.O20o. 10oO22. eoO20o2. coO1fo. d.oO1eo.2 b.3oO1eo. eOo3O1bo. 11.o2O1ao. 14oO18o.2 18.3O10o2. 1f.3 .3o.oO4o. 2c.o2. b4", + " 57.2 270. 17.3 17.3 111.2 30.3 198", + " 410.2 30.2 1a.2 17c", + " 5dc", + " 58a.2 31.2 1d" + ] +].map(frame => frame.map(s => { + let result = [], re = /([ .oO])([\da-f]*)/g, m + while (m = re.exec(s)) { + let v = " .oO".indexOf(m[1]), c = parseInt(m[2] || "1", 16) + for (let i = 0; i < c; i++) result.push(v) + } + return result +})) diff --git a/html/code/hello.js b/html/code/hello.js new file mode 100644 index 000000000..619a8c516 --- /dev/null +++ b/html/code/hello.js @@ -0,0 +1 @@ +alert("hello!"); diff --git a/html/code/index.html b/html/code/index.html new file mode 100644 index 000000000..6762d902d --- /dev/null +++ b/html/code/index.html @@ -0,0 +1,59 @@ + + + + + Eloquent JavaScript :: Code Sandbox + + + + + + + + +
+

Code Sandbox
Eloquent JavaScript

+ +

You can use this page to download source code and solutions to + exercises for the book Eloquent JavaScript, and to directly run code + in the context of chapters from that book, either to solve exercises + to simply play around.

+ +

+ Chapter: + + +

+ +
+
+
+
+ +
+ To run this chapter's code locally, use these files: +
    +
    + +
    + These files contain this chapter’s project code: +
      +
      + +

      If you've solved the exercise and want to compare your code with + mine, or you really tried, but can't get your code to work, + you can (or download it).

      + +

      + The base environment for this chapter (if any) is available in the + sandbox above, allowing you to run the chapter's examples by + simply pasting them into the editor. +

      +
      + + diff --git a/html/code/intro.js b/html/code/intro.js new file mode 100644 index 000000000..5b20af43a --- /dev/null +++ b/html/code/intro.js @@ -0,0 +1,28 @@ +function range(start, end, step) { + if (step == null) step = 1; + var array = []; + + if (step > 0) { + for (var i = start; i <= end; i += step) + array.push(i); + } else { + for (var i = start; i >= end; i += step) + array.push(i); + } + return array; +} + +function sum(array) { + var total = 0; + for (var i = 0; i < array.length; i++) + total += array[i]; + return total; +} + +function factorial(n) { + if (n == 0) { + return 1; + } else { + return factorial(n - 1) * n; + } +} diff --git a/html/code/journal.js b/html/code/journal.js new file mode 100644 index 000000000..ee98d227f --- /dev/null +++ b/html/code/journal.js @@ -0,0 +1,99 @@ +var JOURNAL = [ + {"events":["carrot","exercise","weekend"],"squirrel":false}, + {"events":["bread","pudding","brushed teeth","weekend","touched tree"],"squirrel":false}, + {"events":["carrot","nachos","brushed teeth","cycling","weekend"],"squirrel":false}, + {"events":["brussel sprouts","ice cream","brushed teeth","computer","weekend"],"squirrel":false}, + {"events":["potatoes","candy","brushed teeth","exercise","weekend","dentist"],"squirrel":false}, + {"events":["brussel sprouts","pudding","brushed teeth","running","weekend"],"squirrel":false}, + {"events":["pizza","brushed teeth","computer","work","touched tree"],"squirrel":false}, + {"events":["bread","beer","brushed teeth","cycling","work"],"squirrel":false}, + {"events":["cauliflower","brushed teeth","work"],"squirrel":false}, + {"events":["pizza","brushed teeth","cycling","work"],"squirrel":false}, + {"events":["lasagna","nachos","brushed teeth","work"],"squirrel":false}, + {"events":["brushed teeth","weekend","touched tree"],"squirrel":false}, + {"events":["lettuce","brushed teeth","television","weekend"],"squirrel":false}, + {"events":["spaghetti","brushed teeth","work"],"squirrel":false}, + {"events":["brushed teeth","computer","work"],"squirrel":false}, + {"events":["lettuce","nachos","brushed teeth","work"],"squirrel":false}, + {"events":["carrot","brushed teeth","running","work"],"squirrel":false}, + {"events":["brushed teeth","work"],"squirrel":false}, + {"events":["cauliflower","reading","weekend"],"squirrel":false}, + {"events":["bread","brushed teeth","weekend"],"squirrel":false}, + {"events":["lasagna","brushed teeth","exercise","work"],"squirrel":false}, + {"events":["spaghetti","brushed teeth","reading","work"],"squirrel":false}, + {"events":["carrot","ice cream","brushed teeth","television","work"],"squirrel":false}, + {"events":["spaghetti","nachos","work"],"squirrel":false}, + {"events":["cauliflower","ice cream","brushed teeth","cycling","work"],"squirrel":false}, + {"events":["spaghetti","peanuts","computer","weekend"],"squirrel":true}, + {"events":["potatoes","ice cream","brushed teeth","computer","weekend"],"squirrel":false}, + {"events":["potatoes","ice cream","brushed teeth","work"],"squirrel":false}, + {"events":["peanuts","brushed teeth","running","work"],"squirrel":false}, + {"events":["potatoes","exercise","work"],"squirrel":false}, + {"events":["pizza","ice cream","computer","work"],"squirrel":false}, + {"events":["lasagna","ice cream","work"],"squirrel":false}, + {"events":["cauliflower","candy","reading","weekend"],"squirrel":false}, + {"events":["lasagna","nachos","brushed teeth","running","weekend"],"squirrel":false}, + {"events":["potatoes","brushed teeth","work"],"squirrel":false}, + {"events":["carrot","work"],"squirrel":false}, + {"events":["pizza","beer","work","dentist"],"squirrel":false}, + {"events":["lasagna","pudding","cycling","work"],"squirrel":false}, + {"events":["spaghetti","brushed teeth","reading","work"],"squirrel":false}, + {"events":["spaghetti","pudding","television","weekend"],"squirrel":false}, + {"events":["bread","brushed teeth","exercise","weekend"],"squirrel":false}, + {"events":["lasagna","peanuts","work"],"squirrel":true}, + {"events":["pizza","work"],"squirrel":false}, + {"events":["potatoes","exercise","work"],"squirrel":false}, + {"events":["brushed teeth","exercise","work"],"squirrel":false}, + {"events":["spaghetti","brushed teeth","television","work"],"squirrel":false}, + {"events":["pizza","cycling","weekend"],"squirrel":false}, + {"events":["carrot","brushed teeth","weekend"],"squirrel":false}, + {"events":["carrot","beer","brushed teeth","work"],"squirrel":false}, + {"events":["pizza","peanuts","candy","work"],"squirrel":true}, + {"events":["carrot","peanuts","brushed teeth","reading","work"],"squirrel":false}, + {"events":["potatoes","peanuts","brushed teeth","work"],"squirrel":false}, + {"events":["carrot","nachos","brushed teeth","exercise","work"],"squirrel":false}, + {"events":["pizza","peanuts","brushed teeth","television","weekend"],"squirrel":false}, + {"events":["lasagna","brushed teeth","cycling","weekend"],"squirrel":false}, + {"events":["cauliflower","peanuts","brushed teeth","computer","work","touched tree"],"squirrel":false}, + {"events":["lettuce","brushed teeth","television","work"],"squirrel":false}, + {"events":["potatoes","brushed teeth","computer","work"],"squirrel":false}, + {"events":["bread","candy","work"],"squirrel":false}, + {"events":["potatoes","nachos","work"],"squirrel":false}, + {"events":["carrot","pudding","brushed teeth","weekend"],"squirrel":false}, + {"events":["carrot","brushed teeth","exercise","weekend","touched tree"],"squirrel":false}, + {"events":["brussel sprouts","running","work"],"squirrel":false}, + {"events":["brushed teeth","work"],"squirrel":false}, + {"events":["lettuce","brushed teeth","running","work"],"squirrel":false}, + {"events":["candy","brushed teeth","work"],"squirrel":false}, + {"events":["brussel sprouts","brushed teeth","computer","work"],"squirrel":false}, + {"events":["bread","brushed teeth","weekend"],"squirrel":false}, + {"events":["cauliflower","brushed teeth","weekend"],"squirrel":false}, + {"events":["spaghetti","candy","television","work","touched tree"],"squirrel":false}, + {"events":["carrot","pudding","brushed teeth","work"],"squirrel":false}, + {"events":["lettuce","brushed teeth","work"],"squirrel":false}, + {"events":["carrot","ice cream","brushed teeth","cycling","work"],"squirrel":false}, + {"events":["pizza","brushed teeth","work"],"squirrel":false}, + {"events":["spaghetti","peanuts","exercise","weekend"],"squirrel":true}, + {"events":["bread","beer","computer","weekend","touched tree"],"squirrel":false}, + {"events":["brushed teeth","running","work"],"squirrel":false}, + {"events":["lettuce","peanuts","brushed teeth","work","touched tree"],"squirrel":false}, + {"events":["lasagna","brushed teeth","television","work"],"squirrel":false}, + {"events":["cauliflower","brushed teeth","running","work"],"squirrel":false}, + {"events":["carrot","brushed teeth","running","work"],"squirrel":false}, + {"events":["carrot","reading","weekend"],"squirrel":false}, + {"events":["carrot","peanuts","reading","weekend"],"squirrel":true}, + {"events":["potatoes","brushed teeth","running","work"],"squirrel":false}, + {"events":["lasagna","ice cream","work","touched tree"],"squirrel":false}, + {"events":["cauliflower","peanuts","brushed teeth","cycling","work"],"squirrel":false}, + {"events":["pizza","brushed teeth","running","work"],"squirrel":false}, + {"events":["lettuce","brushed teeth","work"],"squirrel":false}, + {"events":["bread","brushed teeth","television","weekend"],"squirrel":false}, + {"events":["cauliflower","peanuts","brushed teeth","weekend"],"squirrel":false} +]; + +// This makes sure the data is exported in node.js — +// `require('./path/to/journal.js')` will get you the array. +if (typeof module != "undefined" && module.exports && (typeof window == "undefined" || window.exports != exports)) + module.exports = JOURNAL; +if (typeof global != "undefined" && !global.JOURNAL) + global.JOURNAL = JOURNAL; diff --git a/html/code/levels.js b/html/code/levels.js new file mode 100644 index 000000000..cfe887b0c --- /dev/null +++ b/html/code/levels.js @@ -0,0 +1,178 @@ +var GAME_LEVELS = [` +................................................................................ +................................................................................ +................................................................................ +................................................................................ +................................................................................ +................................................................................ +..................................................................###........... +...................................................##......##....##+##.......... +....................................o.o......##..................#+++#.......... +.................................................................##+##.......... +...................................#####..........................#v#........... +............................................................................##.. +..##......................................o.o................................#.. +..#.....................o....................................................#.. +..#......................................#####.............................o.#.. +..#..........####.......o....................................................#.. +..#..@.......#..#................................................#####.......#.. +..############..###############...####################.....#######...#########.. +..............................#...#..................#.....#.................... +..............................#+++#..................#+++++#.................... +..............................#+++#..................#+++++#.................... +..............................#####..................#######.................... +................................................................................ +................................................................................ +`,` +................................................................................ +................................................................................ +....###############################............................................. +...##.............................##########################################.... +...#.......................................................................##... +...#....o...................................................................#... +...#................................................=.......................#... +...#.o........################...................o..o...........|........o..#... +...#.........................#..............................................#... +...#....o....................##########.....###################....##########... +...#..................................#+++++#.................#....#............ +...###############....oo......=o.o.o..#######.###############.#....#............ +.....#...............o..o.............#.......#......#........#....#............ +.....#....................#############..######.####.#.########....########..... +.....#.............########..............#...........#.#..................#..... +.....#..........####......####...#####################.#..................#..... +.....#........###............###.......................########....########..... +.....#.......##................#########################......#....#............ +.....#.......#................................................#....#............ +.....###......................................................#....#............ +.......#...............o...........................................#............ +.......#...............................................o...........#............ +.......#########......###.....############.........................##........... +.............#..................#........#####....#######.o.........########.... +.............#++++++++++++++++++#............#....#.....#..................#.... +.............#++++++++++++++++++#..........###....###...####.o.............#.... +.............####################..........#........#......#.....|.........#.... +...........................................#++++++++#......####............#.... +...........................................#++++++++#.........#........@...#.... +...........................................#++++++++#.........##############.... +...........................................##########........................... +................................................................................ +`,` +......................................#++#........................#######....................................#+#.. +......................................#++#.....................####.....####.................................#+#.. +......................................#++##########...........##...........##................................#+#.. +......................................##++++++++++##.........##.............##...............................#+#.. +.......................................##########++#.........#....................................o...o...o..#+#.. +................................................##+#.........#.....o...o....................................##+#.. +.................................................#+#.........#................................###############++#.. +.................................................#v#.........#.....#...#........................++++++++++++++##.. +.............................................................##..|...|...|..##............#####################... +..............................................................##+++++++++++##............v........................ +...............................................................####+++++####...................................... +...............................................#.....#............#######........###.........###.................. +...............................................#.....#...........................#.#.........#.#.................. +...............................................#.....#.............................#.........#.................... +...............................................#.....#.............................##........#.................... +...............................................##....#.............................#.........#.................... +...............................................#.....#......o..o.....#...#.........#.........#.................... +...............#######........###...###........#.....#...............#...#.........#.........#.................... +..............##.....##.........#...#..........#.....#.....######....#...#...#########.......#.................... +.............##.......##........#.o.#..........#....##...............#...#...#...............#.................... +.....@.......#.........#........#...#..........#.....#...............#...#...#...............#.................... +....###......#.........#........#...#..........#.....#...............#...#####...######......#.................... +....#.#......#.........#.......##.o.##.........#.....#...............#.....o.....#.#.........#.................... +++++#.#++++++#.........#++++++##.....##++++++++##....#++++++++++.....#.....=.....#.#.........#.................... +++++#.#++++++#.........#+++++##.......##########.....#+++++++##+.....#############.##..o.o..##.................... +++++#.#++++++#.........#+++++#....o.................##++++++##.+....................##.....##..................... +++++#.#++++++#.........#+++++#.....................##++++++##..+.....................#######...................... +++++#.#++++++#.........#+++++##.......##############++++++##...+.................................................. +++++#.#++++++#.........#++++++#########++++++++++++++++++##....+.................................................. +++++#.#++++++#.........#++++++++++++++++++++++++++++++++##.....+++++++++++++++++++++++++++++++++++++++++++++++++++ +`,` +.............................................................................................................. +.............................................................................................................. +.............................................................................................................. +.............................................................................................................. +.............................................................................................................. +........................................o..................................................................... +.............................................................................................................. +........................................#..................................................................... +........................................#..................................................................... +........................................#..................................................................... +........................................#..................................................................... +.......................................###.................................................................... +.......................................#.#.................+++........+++..###................................ +.......................................#.#.................+#+........+#+..................................... +.....................................###.###................#..........#...................................... +......................................#...#.................#...oooo...#.......###............................ +......................................#...#.................#..........#......#+++#........................... +......................................#...#.................############.......###............................ +.....................................##...##......#...#......#................................................ +......................................#...#########...########..............#.#............................... +......................................#...#...........#....................#+++#.............................. +......................................#...#...........#.....................###............................... +.....................................##...##..........#....................................................... +......................................#...#=.=.=.=....#............###........................................ +......................................#...#...........#...........#+++#....................................... +......................................#...#....=.=.=.=#.....o......###.......###.............................. +.....................................##...##..........#.....................#+++#............................. +..............................o...o...#...#...........#.....#................##v........###................... +......................................#...#...........#..............#.................#+++#.................. +.............................###.###.###.###.....o.o..#++++++++++++++#...................v#................... +.............................#.###.#.#.###.#..........#++++++++++++++#........................................ +.............................#.............#...#######################........................................ +.............................##...........##.........................................###...................... +..###.........................#.....#.....#.........................................#+++#................###.. +..#.#.........................#....###....#..........................................###.................#.#.. +..#...........................#....###....#######........................#####.............................#.. +..#...........................#...........#..............................#...#.............................#.. +..#...........................##..........#..............................#.#.#.............................#.. +..#.......................................#.......|####|....|####|.....###.###.............................#.. +..#................###.............o.o....#..............................#.........###.....................#.. +..#...............#####.......##..........#.............................###.......#+++#..........#.........#.. +..#...............o###o.......#....###....#.............................#.#........###..........###........#.. +..#................###........#############..#.oo.#....#.oo.#....#.oo..##.##....................###........#.. +..#......@..........#.........#...........#++#....#++++#....#++++#....##...##....................#.........#.. +..#############################...........#############################.....################################.. +.............................................................................................................. +.............................................................................................................. +`,` +..................................................................................................###.#....... +......................................................................................................#....... +..................................................................................................#####....... +..................................................................................................#........... +..................................................................................................#.###....... +..........................o.......................................................................#.#.#....... +.............................................................................................o.o.o###.#....... +...................###................................................................................#....... +.......+..o..+................................................#####.#####.#####.#####.#####.#####.#####....... +.......#.....#................................................#...#.#...#.#...#.#...#.#...#.#...#.#........... +.......#=.o..#............#...................................###.#.###.#.###.#.###.#.###.#.###.#.#####....... +.......#.....#..................................................#.#...#.#...#.#...#.#...#.#...#.#.....#....... +.......+..o..+............o..................................####.#####.#####.#####.#####.#####.#######....... +.............................................................................................................. +..........o..............###..............................##.................................................. +.............................................................................................................. +.............................................................................................................. +......................................................##...................................................... +...................###.........###............................................................................ +.............................................................................................................. +..........................o.....................................................#......#...................... +..........................................................##.....##........................................... +.............###.........###.........###.................................#..................#................. +.............................................................................................................. +.................................................................||........................................... +..###########................................................................................................. +..#.........#.o.#########.o.#########.o.##................................................#................... +..#.........#...#.......#...#.......#...#.................||..................#.....#......................... +..#..@......#####...o...#####...o...#####..................................................................... +..#######.....................................#####.......##.....##.....###................................... +........#=..................=................=#...#.....................###................................... +........#######################################...#+++++++++++++++++++++###+++++++++++++++++++++++++++++++++++ +..................................................############################################################ +.............................................................................................................. +`]; + +if (typeof module != "undefined" && module.exports && (typeof window == "undefined" || window.exports != exports)) + module.exports = GAME_LEVELS; +if (typeof global != "undefined" && !global.GAME_LEVELS) + global.GAME_LEVELS = GAME_LEVELS; diff --git a/html/code/load.js b/html/code/load.js new file mode 100644 index 000000000..bf44c1080 --- /dev/null +++ b/html/code/load.js @@ -0,0 +1,9 @@ +// Since the code for most chapter in Eloquent JavaScript isn't +// written with node's module system in mind, this kludge is used to +// load dependency files into the global namespace, so that the +// examples can run on node. + +module.exports = function(...args) { + for (let arg of args) + (1,eval)(require("fs").readFileSync(__dirname + "/../" + arg, "utf8")) +} diff --git a/html/code/packages_chapter_10.js b/html/code/packages_chapter_10.js new file mode 100644 index 000000000..b4e07bbb9 --- /dev/null +++ b/html/code/packages_chapter_10.js @@ -0,0 +1,520 @@ +/* ordinal 1.0.2: https://github.com/dcousens/ordinal/ + +Copyright (c) 2016, Daniel Cousens + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ + +require.preload("ordinal", String.raw` +function indicator (i) { + var cent = i % 100 + if (cent >= 10 && cent <= 20) return 'th' + var dec = i % 10 + if (dec === 1) return 'st' + if (dec === 2) return 'nd' + if (dec === 3) return 'rd' + return 'th' +} + +function ordinal (i) { + if (typeof i !== 'number') throw new TypeError('Expected Number, got ' + (typeof i) + ' ' + i) + return i + indicator(i) +} + +ordinal.indicator = indicator +module.exports = ordinal`) + +/* date-names 0.1.11: https://github.com/martinandert/date-names + +The MIT License (MIT) + +Copyright (c) 2014 Martin Andert + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + +require.preload("date-names", String.raw` +module.exports = { + __locale: "en", + days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], + abbreviated_days: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], + abbreviated_months: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + am: 'AM', + pm: 'PM' +};`) + +require.preload("./dayname.js", String.raw` +const names = ["Sunday", "Monday", "Tuesday", "Wednesday", + "Thursday", "Friday", "Saturday"]; + +exports.dayName = function(number) { + return names[number]; +} +exports.dayNumber = function(name) { + return names.indexOf(name); +}`) + +require.preload("./seasonname.js", String.raw` +module.exports = ["Winter", "Spring", "Summer", "Autumn"];`) + +/* ini 1.3.5: https://github.com/npm/ini + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ + +require.preload("ini", String.raw`exports.parse = exports.decode = decode + +exports.stringify = exports.encode = encode + +exports.safe = safe +exports.unsafe = unsafe + +var eol = typeof process !== 'undefined' && + process.platform === 'win32' ? '\r\n' : '\n' + +function encode (obj, opt) { + var children = [] + var out = '' + + if (typeof opt === 'string') { + opt = { + section: opt, + whitespace: false + } + } else { + opt = opt || {} + opt.whitespace = opt.whitespace === true + } + + var separator = opt.whitespace ? ' = ' : '=' + + Object.keys(obj).forEach(function (k, _, __) { + var val = obj[k] + if (val && Array.isArray(val)) { + val.forEach(function (item) { + out += safe(k + '[]') + separator + safe(item) + '\n' + }) + } else if (val && typeof val === 'object') { + children.push(k) + } else { + out += safe(k) + separator + safe(val) + eol + } + }) + + if (opt.section && out.length) { + out = '[' + safe(opt.section) + ']' + eol + out + } + + children.forEach(function (k, _, __) { + var nk = dotSplit(k).join('\\.') + var section = (opt.section ? opt.section + '.' : '') + nk + var child = encode(obj[k], { + section: section, + whitespace: opt.whitespace + }) + if (out.length && child.length) { + out += eol + } + out += child + }) + + return out +} + +function dotSplit (str) { + return str.replace(/\1/g, '\u0002LITERAL\\1LITERAL\u0002') + .replace(/\\\./g, '\u0001') + .split(/\./).map(function (part) { + return part.replace(/\1/g, '\\.') + .replace(/\2LITERAL\\1LITERAL\2/g, '\u0001') + }) +} + +function decode (str) { + var out = {} + var p = out + var section = null + // section |key = value + var re = /^\[([^\]]*)\]$|^([^=]+)(=(.*))?$/i + var lines = str.split(/[\r\n]+/g) + + lines.forEach(function (line, _, __) { + if (!line || line.match(/^\s*[;#]/)) return + var match = line.match(re) + if (!match) return + if (match[1] !== undefined) { + section = unsafe(match[1]) + p = out[section] = out[section] || {} + return + } + var key = unsafe(match[2]) + var value = match[3] ? unsafe(match[4]) : true + switch (value) { + case 'true': + case 'false': + case 'null': value = JSON.parse(value) + } + + // Convert keys with '[]' suffix to an array + if (key.length > 2 && key.slice(-2) === '[]') { + key = key.substring(0, key.length - 2) + if (!p[key]) { + p[key] = [] + } else if (!Array.isArray(p[key])) { + p[key] = [p[key]] + } + } + + // safeguard against resetting a previously defined + // array by accidentally forgetting the brackets + if (Array.isArray(p[key])) { + p[key].push(value) + } else { + p[key] = value + } + }) + + // {a:{y:1},"a.b":{x:2}} --> {a:{y:1,b:{x:2}}} + // use a filter to return the keys that have to be deleted. + Object.keys(out).filter(function (k, _, __) { + if (!out[k] || + typeof out[k] !== 'object' || + Array.isArray(out[k])) { + return false + } + // see if the parent section is also an object. + // if so, add it to that, and mark this one for deletion + var parts = dotSplit(k) + var p = out + var l = parts.pop() + var nl = l.replace(/\\\./g, '.') + parts.forEach(function (part, _, __) { + if (!p[part] || typeof p[part] !== 'object') p[part] = {} + p = p[part] + }) + if (p === out && nl === l) { + return false + } + p[nl] = out[k] + return true + }).forEach(function (del, _, __) { + delete out[del] + }) + + return out +} + +function isQuoted (val) { + return (val.charAt(0) === '"' && val.slice(-1) === '"') || + (val.charAt(0) === "'" && val.slice(-1) === "'") +} + +function safe (val) { + return (typeof val !== 'string' || + val.match(/[=\r\n]/) || + val.match(/^\[/) || + (val.length > 1 && + isQuoted(val)) || + val !== val.trim()) + ? JSON.stringify(val) + : val.replace(/;/g, '\\;').replace(/#/g, '\\#') +} + +function unsafe (val, doUnesc) { + val = (val || '').trim() + if (isQuoted(val)) { + // remove the single quotes before calling JSON.parse + if (val.charAt(0) === "'") { + val = val.substr(1, val.length - 2) + } + try { val = JSON.parse(val) } catch (_) {} + } else { + // walk the val to find the first not-escaped ; character + var esc = false + var unesc = '' + for (var i = 0, l = val.length; i < l; i++) { + var c = val.charAt(i) + if (esc) { + if ('\\;#'.indexOf(c) !== -1) { + unesc += c + } else { + unesc += '\\' + c + } + esc = false + } else if (';#'.indexOf(c) !== -1) { + break + } else if (c === '\\') { + esc = true + } else { + unesc += c + } + } + if (esc) { + unesc += '\\' + } + return unesc.trim() + } + return val +}`) + +/* dijkstrajs 1.0.1: https://github.com/tcort/dijkstrajs/ + +Dijkstra path-finding functions. Adapted from the Dijkstar Python project. + +Copyright (C) 2008 + Wyatt Baldwin + All rights reserved + +Licensed under the MIT license. + + http://www.opensource.org/licenses/mit-license.php + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +require.preload("dijkstrajs", String.raw`'use strict'; + +var dijkstra = { + single_source_shortest_paths: function(graph, s, d) { + // Predecessor map for each node that has been encountered. + // node ID => predecessor node ID + var predecessors = {}; + + // Costs of shortest paths from s to all nodes encountered. + // node ID => cost + var costs = {}; + costs[s] = 0; + + // Costs of shortest paths from s to all nodes encountered; differs from + // costs in that it provides easy access to the node that currently has + // the known shortest path from s. + // XXX: Do we actually need both costs and open? + var open = dijkstra.PriorityQueue.make(); + open.push(s, 0); + + var closest, + u, v, + cost_of_s_to_u, + adjacent_nodes, + cost_of_e, + cost_of_s_to_u_plus_cost_of_e, + cost_of_s_to_v, + first_visit; + while (!open.empty()) { + // In the nodes remaining in graph that have a known cost from s, + // find the node, u, that currently has the shortest path from s. + closest = open.pop(); + u = closest.value; + cost_of_s_to_u = closest.cost; + + // Get nodes adjacent to u... + adjacent_nodes = graph[u] || {}; + + // ...and explore the edges that connect u to those nodes, updating + // the cost of the shortest paths to any or all of those nodes as + // necessary. v is the node across the current edge from u. + for (v in adjacent_nodes) { + if (adjacent_nodes.hasOwnProperty(v)) { + // Get the cost of the edge running from u to v. + cost_of_e = adjacent_nodes[v]; + + // Cost of s to u plus the cost of u to v across e--this is *a* + // cost from s to v that may or may not be less than the current + // known cost to v. + cost_of_s_to_u_plus_cost_of_e = cost_of_s_to_u + cost_of_e; + + // If we haven't visited v yet OR if the current known cost from s to + // v is greater than the new cost we just found (cost of s to u plus + // cost of u to v across e), update v's cost in the cost list and + // update v's predecessor in the predecessor list (it's now u). + cost_of_s_to_v = costs[v]; + first_visit = (typeof costs[v] === 'undefined'); + if (first_visit || cost_of_s_to_v > cost_of_s_to_u_plus_cost_of_e) { + costs[v] = cost_of_s_to_u_plus_cost_of_e; + open.push(v, cost_of_s_to_u_plus_cost_of_e); + predecessors[v] = u; + } + } + } + } + + if (typeof d !== 'undefined' && typeof costs[d] === 'undefined') { + var msg = ['Could not find a path from ', s, ' to ', d, '.'].join(''); + throw new Error(msg); + } + + return predecessors; + }, + + extract_shortest_path_from_predecessor_list: function(predecessors, d) { + var nodes = []; + var u = d; + var predecessor; + while (u) { + nodes.push(u); + predecessor = predecessors[u]; + u = predecessors[u]; + } + nodes.reverse(); + return nodes; + }, + + find_path: function(graph, s, d) { + var predecessors = dijkstra.single_source_shortest_paths(graph, s, d); + return dijkstra.extract_shortest_path_from_predecessor_list( + predecessors, d); + }, + + /** + * A very naive priority queue implementation. + */ + PriorityQueue: { + make: function (opts) { + var T = dijkstra.PriorityQueue, + t = {}, + key; + opts = opts || {}; + for (key in T) { + if (T.hasOwnProperty(key)) { + t[key] = T[key]; + } + } + t.queue = []; + t.sorter = opts.sorter || T.default_sorter; + return t; + }, + + default_sorter: function (a, b) { + return a.cost - b.cost; + }, + + /** + * Add a new item to the queue and ensure the highest priority element + * is at the front of the queue. + */ + push: function (value, cost) { + var item = {value: value, cost: cost}; + this.queue.push(item); + this.queue.sort(this.sorter); + }, + + /** + * Return the highest priority element in the queue. + */ + pop: function () { + return this.queue.shift(); + }, + + empty: function () { + return this.queue.length === 0; + } + } +}; + + +// node.js module exports +if (typeof module !== 'undefined') { + module.exports = dijkstra; +}`) + +/* random-item 1.0.0: https://github.com/sindresorhus/random-item + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +require.preload("random-item", String.raw`'use strict'; +module.exports = function (arr) { + if (!Array.isArray(arr)) { + throw new TypeError('Expected an array'); + } + + return arr[Math.floor(Math.random() * arr.length)]; +};`) + +require.preload("./format-date.js", String.raw`const ordinal = require("ordinal"); +const {days, months} = require("date-names"); + +exports.formatDate = function(date, format) { + return format.replace(/YYYY|M(MMM)?|Do?|dddd/g, tag => { + if (tag == "YYYY") return date.getFullYear(); + if (tag == "M") return date.getMonth(); + if (tag == "MMMM") return months[date.getMonth()]; + if (tag == "D") return date.getDate(); + if (tag == "Do") return ordinal(date.getDate()); + if (tag == "dddd") return days[date.getDay()]; + }); +};`) + +require.preload("./graph.js", String.raw`exports.buildGraph = function(edges) { + let graph = Object.create(null); + function addEdge(from, to) { + if (!(from in graph)) graph[from] = Object.create(null); + graph[from][to] = 1; + } + for (let [from, to] of edges) { + addEdge(from, to); + addEdge(to, from); + } + return graph; +};`) diff --git a/html/code/scripts.js b/html/code/scripts.js new file mode 100644 index 000000000..a58105b27 --- /dev/null +++ b/html/code/scripts.js @@ -0,0 +1,1123 @@ +// Generated from the Unicode 10 database and https://en.wikipedia.org/wiki/Script_(Unicode) + +var SCRIPTS = [ + { + name: "Adlam", + ranges: [[125184, 125259], [125264, 125274], [125278, 125280]], + direction: "rtl", + year: 1987, + living: true, + link: "https://en.wikipedia.org/wiki/Fula_alphabets#Adlam_alphabet" + }, + { + name: "Caucasian Albanian", + ranges: [[66864, 66916], [66927, 66928]], + direction: "ltr", + year: 420, + living: false, + link: "https://en.wikipedia.org/wiki/Caucasian_Albanian_alphabet" + }, + { + name: "Ahom", + ranges: [[71424, 71450], [71453, 71468], [71472, 71488]], + direction: "ltr", + year: 1250, + living: false, + link: "https://en.wikipedia.org/wiki/Ahom_alphabet" + }, + { + name: "Arabic", + ranges: [[1536, 1541], [1542, 1548], [1549, 1563], [1564, 1565], [1566, 1567], [1568, 1600], [1601, 1611], [1622, 1648], [1649, 1757], [1758, 1792], [1872, 1920], [2208, 2229], [2230, 2238], [2260, 2274], [2275, 2304], [64336, 64450], [64467, 64830], [64848, 64912], [64914, 64968], [65008, 65022], [65136, 65141], [65142, 65277], [69216, 69247], [126464, 126468], [126469, 126496], [126497, 126499], [126500, 126501], [126503, 126504], [126505, 126515], [126516, 126520], [126521, 126522], [126523, 126524], [126530, 126531], [126535, 126536], [126537, 126538], [126539, 126540], [126541, 126544], [126545, 126547], [126548, 126549], [126551, 126552], [126553, 126554], [126555, 126556], [126557, 126558], [126559, 126560], [126561, 126563], [126564, 126565], [126567, 126571], [126572, 126579], [126580, 126584], [126585, 126589], [126590, 126591], [126592, 126602], [126603, 126620], [126625, 126628], [126629, 126634], [126635, 126652], [126704, 126706]], + direction: "rtl", + year: 400, + living: true, + link: "https://en.wikipedia.org/wiki/Arabic_script" + }, + { + name: "Imperial Aramaic", + ranges: [[67648, 67670], [67671, 67680]], + direction: "rtl", + year: 800, + living: false, + link: "https://en.wikipedia.org/wiki/Aramaic_alphabet" + }, + { + name: "Armenian", + ranges: [[1329, 1367], [1369, 1376], [1377, 1416], [1418, 1419], [1421, 1424], [64275, 64280]], + direction: "ltr", + year: 405, + living: true, + link: "https://en.wikipedia.org/wiki/Armenian_alphabet" + }, + { + name: "Avestan", + ranges: [[68352, 68406], [68409, 68416]], + direction: "rtl", + year: 400, + living: false, + link: "https://en.wikipedia.org/wiki/Avestan_alphabet" + }, + { + name: "Balinese", + ranges: [[6912, 6988], [6992, 7037]], + direction: "ltr", + year: 1000, + living: true, + link: "https://en.wikipedia.org/wiki/Balinese_script" + }, + { + name: "Bamum", + ranges: [[42656, 42744], [92160, 92729]], + direction: "ltr", + year: 1896, + living: true, + link: "https://en.wikipedia.org/wiki/Bamum_script" + }, + { + name: "Bassa Vah", + ranges: [[92880, 92910], [92912, 92918]], + direction: "ltr", + year: 1950, + living: false, + link: "https://en.wikipedia.org/wiki/Bassa_alphabet" + }, + { + name: "Batak", + ranges: [[7104, 7156], [7164, 7168]], + direction: "ltr", + year: 1300, + living: true, + link: "https://en.wikipedia.org/wiki/Batak_alphabet" + }, + { + name: "Bengali", + ranges: [[2432, 2436], [2437, 2445], [2447, 2449], [2451, 2473], [2474, 2481], [2482, 2483], [2486, 2490], [2492, 2501], [2503, 2505], [2507, 2511], [2519, 2520], [2524, 2526], [2527, 2532], [2534, 2558]], + direction: "ltr", + year: 1050, + living: true, + link: "https://en.wikipedia.org/wiki/Bengali_alphabet" + }, + { + name: "Bhaiksuki", + ranges: [[72704, 72713], [72714, 72759], [72760, 72774], [72784, 72813]], + direction: "ltr", + year: 1050, + living: false, + link: "https://en.wikipedia.org/wiki/Bhaiksuki_alphabet" + }, + { + name: "Bopomofo", + ranges: [[746, 748], [12549, 12591], [12704, 12731]], + direction: "ltr", + year: 1918, + living: true, + link: "https://en.wikipedia.org/wiki/Bopomofo" + }, + { + name: "Brahmi", + ranges: [[69632, 69710], [69714, 69744], [69759, 69760]], + direction: "ltr", + year: -250, + living: false, + link: "https://en.wikipedia.org/wiki/Brahmi_script" + }, + { + name: "Braille", + ranges: [[10240, 10496]], + direction: "ltr", + year: 1824, + living: true, + link: "https://en.wikipedia.org/wiki/Braille" + }, + { + name: "Buginese", + ranges: [[6656, 6684], [6686, 6688]], + direction: "ltr", + year: 1650, + living: true, + link: "https://en.wikipedia.org/wiki/Lontara_script" + }, + { + name: "Buhid", + ranges: [[5952, 5972]], + direction: "ltr", + year: 1300, + living: true, + link: "https://en.wikipedia.org/wiki/Buhid_alphabet" + }, + { + name: "Chakma", + ranges: [[69888, 69941], [69942, 69956]], + direction: "ltr", + year: 1050, + living: true, + link: "https://en.wikipedia.org/wiki/Chakma_alphabet" + }, + { + name: "Canadian Aboriginal", + ranges: [[5120, 5760], [6320, 6390]], + direction: "ltr", + year: 1840, + living: true, + link: "https://en.wikipedia.org/wiki/Canadian_Aboriginal_syllabics" + }, + { + name: "Carian", + ranges: [[66208, 66257]], + direction: "ltr", + year: -650, + living: false, + link: "https://en.wikipedia.org/wiki/Carian_alphabets" + }, + { + name: "Cham", + ranges: [[43520, 43575], [43584, 43598], [43600, 43610], [43612, 43616]], + direction: "ltr", + year: 750, + living: true, + link: "https://en.wikipedia.org/wiki/Cham_alphabet" + }, + { + name: "Cherokee", + ranges: [[5024, 5110], [5112, 5118], [43888, 43968]], + direction: "ltr", + year: 1820, + living: true, + link: "https://en.wikipedia.org/wiki/Cherokee_syllabary" + }, + { + name: "Coptic", + ranges: [[994, 1008], [11392, 11508], [11513, 11520]], + direction: "ltr", + year: -200, + living: false, + link: "https://en.wikipedia.org/wiki/Coptic_alphabet" + }, + { + name: "Cypriot", + ranges: [[67584, 67590], [67592, 67593], [67594, 67638], [67639, 67641], [67644, 67645], [67647, 67648]], + direction: "rtl", + year: -1100, + living: false, + link: "https://en.wikipedia.org/wiki/Cypriot_syllabary" + }, + { + name: "Cyrillic", + ranges: [[1024, 1157], [1159, 1328], [7296, 7305], [7467, 7468], [7544, 7545], [11744, 11776], [42560, 42656], [65070, 65072]], + direction: "ltr", + year: 950, + living: true, + link: "https://en.wikipedia.org/wiki/Cyrillic_script" + }, + { + name: "Devanagari", + ranges: [[2304, 2385], [2387, 2404], [2406, 2432], [43232, 43262]], + direction: "ltr", + year: 100, + living: true, + link: "https://en.wikipedia.org/wiki/Devanagari" + }, + { + name: "Deseret", + ranges: [[66560, 66640]], + direction: "ltr", + year: 1854, + living: true, + link: "https://en.wikipedia.org/wiki/Deseret_alphabet" + }, + { + name: "Duployan", + ranges: [[113664, 113771], [113776, 113789], [113792, 113801], [113808, 113818], [113820, 113824]], + direction: "ltr", + year: 1860, + living: true, + link: "https://en.wikipedia.org/wiki/Duployan_shorthand" + }, + { + name: "Egyptian Hieroglyphs", + ranges: [[77824, 78895]], + direction: "ltr", + year: -3200, + living: false, + link: "https://en.wikipedia.org/wiki/Egyptian_hieroglyphs" + }, + { + name: "Elbasan", + ranges: [[66816, 66856]], + direction: "ltr", + year: 1750, + living: false, + link: "https://en.wikipedia.org/wiki/Elbasan_alphabet" + }, + { + name: "Ethiopic", + ranges: [[4608, 4681], [4682, 4686], [4688, 4695], [4696, 4697], [4698, 4702], [4704, 4745], [4746, 4750], [4752, 4785], [4786, 4790], [4792, 4799], [4800, 4801], [4802, 4806], [4808, 4823], [4824, 4881], [4882, 4886], [4888, 4955], [4957, 4989], [4992, 5018], [11648, 11671], [11680, 11687], [11688, 11695], [11696, 11703], [11704, 11711], [11712, 11719], [11720, 11727], [11728, 11735], [11736, 11743], [43777, 43783], [43785, 43791], [43793, 43799], [43808, 43815], [43816, 43823]], + direction: "ltr", + year: -900, + living: true, + link: "https://en.wikipedia.org/wiki/Ge%27ez_script" + }, + { + name: "Georgian", + ranges: [[4256, 4294], [4295, 4296], [4301, 4302], [4304, 4347], [4348, 4352], [11520, 11558], [11559, 11560], [11565, 11566]], + direction: "ltr", + year: 430, + living: true, + link: "https://en.wikipedia.org/wiki/Georgian_scripts" + }, + { + name: "Glagolitic", + ranges: [[11264, 11311], [11312, 11359], [122880, 122887], [122888, 122905], [122907, 122914], [122915, 122917], [122918, 122923]], + direction: "ltr", + year: 862, + living: false, + link: "https://en.wikipedia.org/wiki/Glagolitic_script" + }, + { + name: "Masaram Gondi", + ranges: [[72960, 72967], [72968, 72970], [72971, 73015], [73018, 73019], [73020, 73022], [73023, 73032], [73040, 73050]], + direction: "ltr", + year: 1918, + living: true, + link: "https://en.wikipedia.org/wiki/Gondi_writing#Masaram" + }, + { + name: "Gothic", + ranges: [[66352, 66379]], + direction: "ltr", + year: 350, + living: false, + link: "https://en.wikipedia.org/wiki/Gothic_alphabet" + }, + { + name: "Grantha", + ranges: [[70400, 70404], [70405, 70413], [70415, 70417], [70419, 70441], [70442, 70449], [70450, 70452], [70453, 70458], [70460, 70469], [70471, 70473], [70475, 70478], [70480, 70481], [70487, 70488], [70493, 70500], [70502, 70509], [70512, 70517]], + direction: "ltr", + year: 550, + living: false, + link: "https://en.wikipedia.org/wiki/Grantha_alphabet" + }, + { + name: "Greek", + ranges: [[880, 884], [885, 888], [890, 894], [895, 896], [900, 901], [902, 903], [904, 907], [908, 909], [910, 930], [931, 994], [1008, 1024], [7462, 7467], [7517, 7522], [7526, 7531], [7615, 7616], [7936, 7958], [7960, 7966], [7968, 8006], [8008, 8014], [8016, 8024], [8025, 8026], [8027, 8028], [8029, 8030], [8031, 8062], [8064, 8117], [8118, 8133], [8134, 8148], [8150, 8156], [8157, 8176], [8178, 8181], [8182, 8191], [8486, 8487], [43877, 43878], [65856, 65935], [65952, 65953], [119296, 119366]], + direction: "ltr", + year: -800, + living: true, + link: "https://en.wikipedia.org/wiki/Greek_alphabet" + }, + { + name: "Gujarati", + ranges: [[2689, 2692], [2693, 2702], [2703, 2706], [2707, 2729], [2730, 2737], [2738, 2740], [2741, 2746], [2748, 2758], [2759, 2762], [2763, 2766], [2768, 2769], [2784, 2788], [2790, 2802], [2809, 2816]], + direction: "ltr", + year: 1592, + living: true, + link: "https://en.wikipedia.org/wiki/Gujarati_alphabet" + }, + { + name: "Gurmukhi", + ranges: [[2561, 2564], [2565, 2571], [2575, 2577], [2579, 2601], [2602, 2609], [2610, 2612], [2613, 2615], [2616, 2618], [2620, 2621], [2622, 2627], [2631, 2633], [2635, 2638], [2641, 2642], [2649, 2653], [2654, 2655], [2662, 2678]], + direction: "ltr", + year: 1550, + living: true, + link: "https://en.wikipedia.org/wiki/Gurmukh%C4%AB_alphabet" + }, + { + name: "Hangul", + ranges: [[4352, 4608], [12334, 12336], [12593, 12687], [12800, 12831], [12896, 12927], [43360, 43389], [44032, 55204], [55216, 55239], [55243, 55292], [65440, 65471], [65474, 65480], [65482, 65488], [65490, 65496], [65498, 65501]], + direction: "ltr", + year: 1443, + living: true, + link: "https://en.wikipedia.org/wiki/Hangul" + }, + { + name: "Han", + ranges: [[11904, 11930], [11931, 12020], [12032, 12246], [12293, 12294], [12295, 12296], [12321, 12330], [12344, 12348], [13312, 19894], [19968, 40939], [63744, 64110], [64112, 64218], [131072, 173783], [173824, 177973], [177984, 178206], [178208, 183970], [183984, 191457], [194560, 195102]], + direction: "ltr", + year: -1100, + living: true, + link: "https://en.wikipedia.org/wiki/Chinese_characters" + }, + { + name: "Hanunoo", + ranges: [[5920, 5941]], + direction: "ltr", + year: 1300, + living: true, + link: "https://en.wikipedia.org/wiki/Hanun%C3%B3%27o_alphabet" + }, + { + name: "Hatran", + ranges: [[67808, 67827], [67828, 67830], [67835, 67840]], + direction: "rtl", + year: -40, + living: false, + link: "https://en.wikipedia.org/wiki/Hatran_alphabet" + }, + { + name: "Hebrew", + ranges: [[1425, 1480], [1488, 1515], [1520, 1525], [64285, 64311], [64312, 64317], [64318, 64319], [64320, 64322], [64323, 64325], [64326, 64336]], + direction: "rtl", + year: -100, + living: true, + link: "https://en.wikipedia.org/wiki/Hebrew_alphabet" + }, + { + name: "Hiragana", + ranges: [[12353, 12439], [12445, 12448], [110593, 110879], [127488, 127489]], + direction: "ltr", + year: 800, + living: true, + link: "https://en.wikipedia.org/wiki/Hiragana" + }, + { + name: "Anatolian Hieroglyphs", + ranges: [[82944, 83527]], + direction: "ltr", + year: -1400, + living: false, + link: "https://en.wikipedia.org/wiki/Anatolian_hieroglyphs" + }, + { + name: "Pahawh Hmong", + ranges: [[92928, 92998], [93008, 93018], [93019, 93026], [93027, 93048], [93053, 93072]], + direction: "ltr", + year: 1959, + living: true, + link: "https://en.wikipedia.org/wiki/Pahawh_Hmong" + }, + { + name: "Old Hungarian", + ranges: [[68736, 68787], [68800, 68851], [68858, 68864]], + direction: "rtl", + year: 1150, + living: false, + link: "https://en.wikipedia.org/wiki/Old_Hungarian_alphabet" + }, + { + name: "Old Italic", + ranges: [[66304, 66340], [66349, 66352]], + direction: "ltr", + year: -750, + living: false, + link: "https://en.wikipedia.org/wiki/Old_Italic_script" + }, + { + name: "Javanese", + ranges: [[43392, 43470], [43472, 43482], [43486, 43488]], + direction: "ltr", + year: 1250, + living: true, + link: "https://en.wikipedia.org/wiki/Javanese_script" + }, + { + name: "Kayah Li", + ranges: [[43264, 43310], [43311, 43312]], + direction: "ltr", + year: 1962, + living: true, + link: "https://en.wikipedia.org/wiki/Kayah_Li_alphabet" + }, + { + name: "Katakana", + ranges: [[12449, 12539], [12541, 12544], [12784, 12800], [13008, 13055], [13056, 13144], [65382, 65392], [65393, 65438], [110592, 110593]], + direction: "ltr", + year: 800, + living: true, + link: "https://en.wikipedia.org/wiki/Katakana" + }, + { + name: "Kharoshthi", + ranges: [[68096, 68100], [68101, 68103], [68108, 68116], [68117, 68120], [68121, 68148], [68152, 68155], [68159, 68168], [68176, 68185]], + direction: "rtl", + year: -400, + living: false, + link: "https://en.wikipedia.org/wiki/Kharosthi" + }, + { + name: "Khmer", + ranges: [[6016, 6110], [6112, 6122], [6128, 6138], [6624, 6656]], + direction: "ltr", + year: 611, + living: true, + link: "https://en.wikipedia.org/wiki/Khmer_alphabet" + }, + { + name: "Khojki", + ranges: [[70144, 70162], [70163, 70207]], + direction: "ltr", + year: 1520, + living: false, + link: "https://en.wikipedia.org/wiki/Khojki_script" + }, + { + name: "Kannada", + ranges: [[3200, 3204], [3205, 3213], [3214, 3217], [3218, 3241], [3242, 3252], [3253, 3258], [3260, 3269], [3270, 3273], [3274, 3278], [3285, 3287], [3294, 3295], [3296, 3300], [3302, 3312], [3313, 3315]], + direction: "ltr", + year: 450, + living: true, + link: "https://en.wikipedia.org/wiki/Kannada_alphabet" + }, + { + name: "Kaithi", + ranges: [[69760, 69826]], + direction: "ltr", + year: 1550, + living: false, + link: "https://en.wikipedia.org/wiki/Kaithi" + }, + { + name: "Tai Tham", + ranges: [[6688, 6751], [6752, 6781], [6783, 6794], [6800, 6810], [6816, 6830]], + direction: "ltr", + year: 1300, + living: true, + link: "https://en.wikipedia.org/wiki/Tai_Tham_alphabet" + }, + { + name: "Lao", + ranges: [[3713, 3715], [3716, 3717], [3719, 3721], [3722, 3723], [3725, 3726], [3732, 3736], [3737, 3744], [3745, 3748], [3749, 3750], [3751, 3752], [3754, 3756], [3757, 3770], [3771, 3774], [3776, 3781], [3782, 3783], [3784, 3790], [3792, 3802], [3804, 3808]], + direction: "ltr", + year: 1350, + living: true, + link: "https://en.wikipedia.org/wiki/Lao_alphabet" + }, + { + name: "Latin", + ranges: [[65, 91], [97, 123], [170, 171], [186, 187], [192, 215], [216, 247], [248, 697], [736, 741], [7424, 7462], [7468, 7517], [7522, 7526], [7531, 7544], [7545, 7615], [7680, 7936], [8305, 8306], [8319, 8320], [8336, 8349], [8490, 8492], [8498, 8499], [8526, 8527], [8544, 8585], [11360, 11392], [42786, 42888], [42891, 42927], [42928, 42936], [42999, 43008], [43824, 43867], [43868, 43877], [64256, 64263], [65313, 65339], [65345, 65371]], + direction: "ltr", + year: -700, + living: true, + link: "https://en.wikipedia.org/wiki/Latin_script" + }, + { + name: "Lepcha", + ranges: [[7168, 7224], [7227, 7242], [7245, 7248]], + direction: "ltr", + year: 1700, + living: true, + link: "https://en.wikipedia.org/wiki/Lepcha_alphabet" + }, + { + name: "Limbu", + ranges: [[6400, 6431], [6432, 6444], [6448, 6460], [6464, 6465], [6468, 6480]], + direction: "ltr", + year: 1740, + living: true, + link: "https://en.wikipedia.org/wiki/Limbu_alphabet" + }, + { + name: "Linear A", + ranges: [[67072, 67383], [67392, 67414], [67424, 67432]], + direction: "ltr", + year: -2500, + living: false, + link: "https://en.wikipedia.org/wiki/Linear_A" + }, + { + name: "Linear B", + ranges: [[65536, 65548], [65549, 65575], [65576, 65595], [65596, 65598], [65599, 65614], [65616, 65630], [65664, 65787]], + direction: "ltr", + year: -1450, + living: false, + link: "https://en.wikipedia.org/wiki/Linear_B" + }, + { + name: "Lisu", + ranges: [[42192, 42240]], + direction: "ltr", + year: 1915, + living: true, + link: "https://en.wikipedia.org/wiki/Fraser_alphabet" + }, + { + name: "Lycian", + ranges: [[66176, 66205]], + direction: "ltr", + year: -500, + living: false, + link: "https://en.wikipedia.org/wiki/Lycian_alphabet" + }, + { + name: "Lydian", + ranges: [[67872, 67898], [67903, 67904]], + direction: "rtl", + year: -700, + living: false, + link: "https://en.wikipedia.org/wiki/Lydian_alphabet" + }, + { + name: "Mahajani", + ranges: [[69968, 70007]], + direction: "ltr", + year: 1150, + living: false, + link: "https://en.wikipedia.org/wiki/Mahajani" + }, + { + name: "Mandaic", + ranges: [[2112, 2140], [2142, 2143]], + direction: "rtl", + year: 200, + living: true, + link: "https://en.wikipedia.org/wiki/Mandaic_alphabet" + }, + { + name: "Manichaean", + ranges: [[68288, 68327], [68331, 68343]], + direction: "rtl", + year: 250, + living: false, + link: "https://en.wikipedia.org/wiki/Manichaean_alphabet" + }, + { + name: "Marchen", + ranges: [[72816, 72848], [72850, 72872], [72873, 72887]], + direction: "ltr", + year: 650, + living: false, + link: "https://en.wikipedia.org/wiki/Zhang-Zhung_language#Scripts" + }, + { + name: "Mende Kikakui", + ranges: [[124928, 125125], [125127, 125143]], + direction: "rtl", + year: 1880, + living: true, + link: "https://en.wikipedia.org/wiki/Mende_Kikakui_script" + }, + { + name: "Meroitic Cursive", + ranges: [[68000, 68024], [68028, 68048], [68050, 68096]], + direction: "rtl", + year: -300, + living: false, + link: "https://en.wikipedia.org/wiki/Meroitic_alphabet" + }, + { + name: "Meroitic Hieroglyphs", + ranges: [[67968, 68000]], + direction: "rtl", + year: -300, + living: false, + link: "https://en.wikipedia.org/wiki/Meroitic_alphabet" + }, + { + name: "Malayalam", + ranges: [[3328, 3332], [3333, 3341], [3342, 3345], [3346, 3397], [3398, 3401], [3402, 3408], [3412, 3428], [3430, 3456]], + direction: "ltr", + year: 830, + living: true, + link: "https://en.wikipedia.org/wiki/Malayalam_script" + }, + { + name: "Modi", + ranges: [[71168, 71237], [71248, 71258]], + direction: "ltr", + year: 1200, + living: false, + link: "https://en.wikipedia.org/wiki/Modi_alphabet" + }, + { + name: "Mongolian", + ranges: [[6144, 6146], [6148, 6149], [6150, 6159], [6160, 6170], [6176, 6264], [6272, 6315], [71264, 71277]], + direction: "ttb", + year: 1204, + living: false, + link: "https://en.wikipedia.org/wiki/Mongolian_script" + }, + { + name: "Mro", + ranges: [[92736, 92767], [92768, 92778], [92782, 92784]], + direction: "ltr", + year: 1985, + living: true, + link: "https://en.wikipedia.org/wiki/Mru_language#Alphabet" + }, + { + name: "Meetei Mayek", + ranges: [[43744, 43767], [43968, 44014], [44016, 44026]], + direction: "ltr", + year: 200, + living: true, + link: "https://en.wikipedia.org/wiki/Meitei_script" + }, + { + name: "Multani", + ranges: [[70272, 70279], [70280, 70281], [70282, 70286], [70287, 70302], [70303, 70314]], + direction: "ltr", + year: 1750, + living: false, + link: "https://en.wikipedia.org/wiki/Multani_alphabet" + }, + { + name: "Myanmar", + ranges: [[4096, 4256], [43488, 43519], [43616, 43648]], + direction: "ltr", + year: 984, + living: true, + link: "https://en.wikipedia.org/wiki/Burmese_alphabet" + }, + { + name: "Old North Arabian", + ranges: [[68224, 68256]], + direction: "rtl", + year: 750, + living: false, + link: "https://en.wikipedia.org/wiki/Ancient_North_Arabian" + }, + { + name: "Nabataean", + ranges: [[67712, 67743], [67751, 67760]], + direction: "rtl", + year: 150, + living: false, + link: "https://en.wikipedia.org/wiki/Nabataean_alphabet" + }, + { + name: "Newa", + ranges: [[70656, 70746], [70747, 70748], [70749, 70750]], + direction: "ltr", + year: 1000, + living: true, + link: "https://en.wikipedia.org/wiki/Prachalit_Nepal_alphabet" + }, + { + name: "Nko", + ranges: [[1984, 2043]], + direction: "rtl", + year: 1949, + living: false, + link: "https://en.wikipedia.org/wiki/N%27Ko_alphabet" + }, + { + name: "Nushu", + ranges: [[94177, 94178], [110960, 111356]], + direction: "ltr", + year: 1500, + living: true, + link: "https://en.wikipedia.org/wiki/N%C3%BCshu_script" + }, + { + name: "Ogham", + ranges: [[5760, 5789]], + direction: "ltr", + year: 350, + living: false, + link: "https://en.wikipedia.org/wiki/Ogham" + }, + { + name: "Ol Chiki", + ranges: [[7248, 7296]], + direction: "ltr", + year: 1925, + living: true, + link: "https://en.wikipedia.org/wiki/Ol_Chiki_script" + }, + { + name: "Old Turkic", + ranges: [[68608, 68681]], + direction: "rtl", + year: 750, + living: false, + link: "https://en.wikipedia.org/wiki/Old_Turkic_alphabet" + }, + { + name: "Oriya", + ranges: [[2817, 2820], [2821, 2829], [2831, 2833], [2835, 2857], [2858, 2865], [2866, 2868], [2869, 2874], [2876, 2885], [2887, 2889], [2891, 2894], [2902, 2904], [2908, 2910], [2911, 2916], [2918, 2936]], + direction: "ltr", + year: 1060, + living: true, + link: "https://en.wikipedia.org/wiki/Odia_alphabet" + }, + { + name: "Osage", + ranges: [[66736, 66772], [66776, 66812]], + direction: "ltr", + year: 2006, + living: true, + link: "https://en.wikipedia.org/wiki/Osage_alphabet" + }, + { + name: "Osmanya", + ranges: [[66688, 66718], [66720, 66730]], + direction: "ltr", + year: 1920, + living: true, + link: "https://en.wikipedia.org/wiki/Osmanya_alphabet" + }, + { + name: "Palmyrene", + ranges: [[67680, 67712]], + direction: "rtl", + year: -100, + living: false, + link: "https://en.wikipedia.org/wiki/Palmyrene_alphabet" + }, + { + name: "Pau Cin Hau", + ranges: [[72384, 72441]], + direction: "ltr", + year: 1900, + living: true, + link: "https://en.wikipedia.org/wiki/Pau_Cin_Hau" + }, + { + name: "Old Permic", + ranges: [[66384, 66427]], + direction: "ltr", + year: 1372, + living: false, + link: "https://en.wikipedia.org/wiki/Old_Permic_alphabet" + }, + { + name: "Phags-pa", + ranges: [[43072, 43123], [43124, 43127]], + direction: "ttb", + year: 1269, + living: false, + link: "https://en.wikipedia.org/wiki/%27Phags-pa_script" + }, + { + name: "Inscriptional Pahlavi", + ranges: [[68448, 68467], [68472, 68480]], + direction: "rtl", + year: -171, + living: false, + link: "https://en.wikipedia.org/wiki/Inscriptional_Pahlavi" + }, + { + name: "Psalter Pahlavi", + ranges: [[68480, 68498], [68505, 68509], [68521, 68528]], + direction: "rtl", + year: 550, + living: false, + link: "https://en.wikipedia.org/wiki/Psalter_Pahlavi" + }, + { + name: "Phoenician", + ranges: [[67840, 67868], [67871, 67872]], + direction: "rtl", + year: -1200, + living: false, + link: "https://en.wikipedia.org/wiki/Phoenician_alphabet" + }, + { + name: "Miao", + ranges: [[93952, 94021], [94032, 94079], [94095, 94112]], + direction: "ltr", + year: 1936, + living: true, + link: "https://en.wikipedia.org/wiki/Pollard_script" + }, + { + name: "Inscriptional Parthian", + ranges: [[68416, 68438], [68440, 68448]], + direction: "rtl", + year: -250, + living: false, + link: "https://en.wikipedia.org/wiki/Inscriptional_Parthian" + }, + { + name: "Rejang", + ranges: [[43312, 43348], [43359, 43360]], + direction: "ltr", + year: 1750, + living: true, + link: "https://en.wikipedia.org/wiki/Rejang_script" + }, + { + name: "Runic", + ranges: [[5792, 5867], [5870, 5881]], + direction: "ltr", + year: 150, + living: false, + link: "https://en.wikipedia.org/wiki/Runes" + }, + { + name: "Samaritan", + ranges: [[2048, 2094], [2096, 2111]], + direction: "rtl", + year: -600, + living: true, + link: "https://en.wikipedia.org/wiki/Samaritan_alphabet" + }, + { + name: "Old South Arabian", + ranges: [[68192, 68224]], + direction: "rtl", + year: -850, + living: false, + link: "https://en.wikipedia.org/wiki/Ancient_South_Arabian_script" + }, + { + name: "Saurashtra", + ranges: [[43136, 43206], [43214, 43226]], + direction: "ltr", + year: 1920, + living: true, + link: "https://en.wikipedia.org/wiki/Saurashtra_alphabet" + }, + { + name: "SignWriting", + ranges: [[120832, 121484], [121499, 121504], [121505, 121520]], + direction: "ttb", + year: 1974, + living: true, + link: "https://en.wikipedia.org/wiki/SignWriting" + }, + { + name: "Shavian", + ranges: [[66640, 66688]], + direction: "ltr", + year: 1960, + living: true, + link: "https://en.wikipedia.org/wiki/Shavian_alphabet" + }, + { + name: "Sharada", + ranges: [[70016, 70094], [70096, 70112]], + direction: "ltr", + year: 800, + living: true, + link: "https://en.wikipedia.org/wiki/%C5%9A%C4%81rad%C4%81_script" + }, + { + name: "Siddham", + ranges: [[71040, 71094], [71096, 71134]], + direction: "ltr", + year: 550, + living: false, + link: "https://en.wikipedia.org/wiki/Siddha%E1%B9%83_script" + }, + { + name: "Khudawadi", + ranges: [[70320, 70379], [70384, 70394]], + direction: "ltr", + year: 1550, + living: true, + link: "https://en.wikipedia.org/wiki/Khudabadi_script" + }, + { + name: "Sinhala", + ranges: [[3458, 3460], [3461, 3479], [3482, 3506], [3507, 3516], [3517, 3518], [3520, 3527], [3530, 3531], [3535, 3541], [3542, 3543], [3544, 3552], [3558, 3568], [3570, 3573], [70113, 70133]], + direction: "ltr", + year: 700, + living: true, + link: "https://en.wikipedia.org/wiki/Sinhalese_alphabet" + }, + { + name: "Sora Sompeng", + ranges: [[69840, 69865], [69872, 69882]], + direction: "ltr", + year: 1936, + living: true, + link: "https://en.wikipedia.org/wiki/Sorang_Sompeng_alphabet" + }, + { + name: "Soyombo", + ranges: [[72272, 72324], [72326, 72349], [72350, 72355]], + direction: "ltr", + year: 1650, + living: false, + link: "https://en.wikipedia.org/wiki/Soyombo_alphabet" + }, + { + name: "Sundanese", + ranges: [[7040, 7104], [7360, 7368]], + direction: "ltr", + year: 1350, + living: true, + link: "https://en.wikipedia.org/wiki/Sundanese_script" + }, + { + name: "Syloti Nagri", + ranges: [[43008, 43052]], + direction: "ltr", + year: 1303, + living: true, + link: "https://en.wikipedia.org/wiki/Sylheti_Nagari" + }, + { + name: "Syriac", + ranges: [[1792, 1806], [1807, 1867], [1869, 1872], [2144, 2155]], + direction: "rtl", + year: -200, + living: true, + link: "https://en.wikipedia.org/wiki/Syriac_alphabet" + }, + { + name: "Tagbanwa", + ranges: [[5984, 5997], [5998, 6001], [6002, 6004]], + direction: "ltr", + year: 1300, + living: true, + link: "https://en.wikipedia.org/wiki/Tagbanwa_script" + }, + { + name: "Takri", + ranges: [[71296, 71352], [71360, 71370]], + direction: "ltr", + year: 1550, + living: true, + link: "https://en.wikipedia.org/wiki/Takri_alphabet" + }, + { + name: "Tai Le", + ranges: [[6480, 6510], [6512, 6517]], + direction: "ltr", + year: 1200, + living: true, + link: "https://en.wikipedia.org/wiki/Tai_Le_alphabet" + }, + { + name: "New Tai Lue", + ranges: [[6528, 6572], [6576, 6602], [6608, 6619], [6622, 6624]], + direction: "ltr", + year: 1950, + living: true, + link: "https://en.wikipedia.org/wiki/New_Tai_Lue_alphabet" + }, + { + name: "Tamil", + ranges: [[2946, 2948], [2949, 2955], [2958, 2961], [2962, 2966], [2969, 2971], [2972, 2973], [2974, 2976], [2979, 2981], [2984, 2987], [2990, 3002], [3006, 3011], [3014, 3017], [3018, 3022], [3024, 3025], [3031, 3032], [3046, 3067]], + direction: "ltr", + year: 700, + living: true, + link: "https://en.wikipedia.org/wiki/Tamil_script" + }, + { + name: "Tangut", + ranges: [[94176, 94177], [94208, 100333], [100352, 101107]], + direction: "ltr", + year: 1036, + living: false, + link: "https://en.wikipedia.org/wiki/Tangut_script" + }, + { + name: "Tai Viet", + ranges: [[43648, 43715], [43739, 43744]], + direction: "ltr", + year: 1200, + living: true, + link: "https://en.wikipedia.org/wiki/Tai_Dam_language#Writing_system" + }, + { + name: "Telugu", + ranges: [[3072, 3076], [3077, 3085], [3086, 3089], [3090, 3113], [3114, 3130], [3133, 3141], [3142, 3145], [3146, 3150], [3157, 3159], [3160, 3163], [3168, 3172], [3174, 3184], [3192, 3200]], + direction: "ltr", + year: -900, + living: true, + link: "https://en.wikipedia.org/wiki/Telugu_script" + }, + { + name: "Tifinagh", + ranges: [[11568, 11624], [11631, 11633], [11647, 11648]], + direction: "ltr", + year: -300, + living: true, + link: "https://en.wikipedia.org/wiki/Tifinagh" + }, + { + name: "Tagalog", + ranges: [[5888, 5901], [5902, 5909]], + direction: "ltr", + year: 1250, + living: true, + link: "https://en.wikipedia.org/wiki/Baybayin" + }, + { + name: "Thaana", + ranges: [[1920, 1970]], + direction: "rtl", + year: 1599, + living: true, + link: "https://en.wikipedia.org/wiki/Thaana" + }, + { + name: "Thai", + ranges: [[3585, 3643], [3648, 3676]], + direction: "ltr", + year: 1283, + living: true, + link: "https://en.wikipedia.org/wiki/Thai_alphabet" + }, + { + name: "Tibetan", + ranges: [[3840, 3912], [3913, 3949], [3953, 3992], [3993, 4029], [4030, 4045], [4046, 4053], [4057, 4059]], + direction: "ltr", + year: 650, + living: false, + link: "https://en.wikipedia.org/wiki/Tibetan_alphabet" + }, + { + name: "Tirhuta", + ranges: [[70784, 70856], [70864, 70874]], + direction: "ltr", + year: 1450, + living: true, + link: "https://en.wikipedia.org/wiki/Tirhuta" + }, + { + name: "Ugaritic", + ranges: [[66432, 66462], [66463, 66464]], + direction: "ltr", + year: -1400, + living: false, + link: "https://en.wikipedia.org/wiki/Ugaritic_alphabet" + }, + { + name: "Vai", + ranges: [[42240, 42540]], + direction: "ltr", + year: 1830, + living: true, + link: "https://en.wikipedia.org/wiki/Vai_syllabary" + }, + { + name: "Warang Citi", + ranges: [[71840, 71923], [71935, 71936]], + direction: "ltr", + year: 1946, + living: true, + link: "https://en.wikipedia.org/wiki/Warang_Citi" + }, + { + name: "Old Persian", + ranges: [[66464, 66500], [66504, 66518]], + direction: "ltr", + year: -525, + living: false, + link: "https://en.wikipedia.org/wiki/Old_Persian_cuneiform" + }, + { + name: "Cuneiform", + ranges: [[73728, 74650], [74752, 74863], [74864, 74869], [74880, 75076]], + direction: "ltr", + year: -3050, + living: false, + link: "https://en.wikipedia.org/wiki/Cuneiform_script" + }, + { + name: "Yi", + ranges: [[40960, 42125], [42128, 42183]], + direction: "ltr", + year: 1450, + living: true, + link: "https://en.wikipedia.org/wiki/Yi_script" + }, + { + name: "Zanabazar Square", + ranges: [[72192, 72264]], + direction: "ltr", + year: 1700, + living: false, + link: "https://en.wikipedia.org/wiki/Mongolian_writing_systems#Horizontal_square_script" + } +]; + +// This makes sure the data is exported in node.js — +// `require('./path/to/scripts.js')` will get you the array. +if (typeof module != "undefined" && module.exports && (typeof window == "undefined" || window.exports != exports)) + module.exports = SCRIPTS; +if (typeof global != "undefined" && !global.SCRIPTS) + global.SCRIPTS = SCRIPTS; diff --git a/html/code/skillsharing.zip b/html/code/skillsharing.zip new file mode 100644 index 000000000..e6042e231 Binary files /dev/null and b/html/code/skillsharing.zip differ diff --git a/html/code/skillsharing/.keep b/html/code/skillsharing/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/html/code/skillsharing/package.json b/html/code/skillsharing/package.json new file mode 100644 index 000000000..8851e3da4 --- /dev/null +++ b/html/code/skillsharing/package.json @@ -0,0 +1,23 @@ +{ + "name": "ejs-skillsharing", + "version": "1.0.0", + "main": "skillsharing_server.js", + "description": "Skill-sharing website example from Eloquent JavaScript", + "dependencies": { + "serve-static": "^1.15.0" + }, + "license": "MIT", + "bugs": "https://github.com/marijnh/Eloquent-JavaScript/issues", + "homepage": "https://eloquentjavascript.net/21_skillsharing.html", + "maintainers": [ + { + "name": "Marijn Haverbeke", + "email": "marijn@haverbeke.berlin", + "web": "https://marijnhaverbeke.nl/" + } + ], + "repository": { + "type": "git", + "url": "https://github.com/marijnh/Eloquent-JavaScript.git" + } +} diff --git a/html/code/skillsharing/public/index.html b/html/code/skillsharing/public/index.html new file mode 100644 index 000000000..47f1fbb5c --- /dev/null +++ b/html/code/skillsharing/public/index.html @@ -0,0 +1,8 @@ + + +Skill Sharing + + +

      Skill Sharing

      + + diff --git a/html/code/skillsharing/public/skillsharing.css b/html/code/skillsharing/public/skillsharing.css new file mode 100644 index 000000000..75da06bbc --- /dev/null +++ b/html/code/skillsharing/public/skillsharing.css @@ -0,0 +1,11 @@ +.talk { margin: 40px 0; } + +.comment { font-style: italic; margin: 0; } +.comment strong { font-style: normal; } + +.talk h2 { font-size: 130%; margin-bottom: 0; } +.talk h2 button { vertical-align: bottom; } + +h1, h3 { margin-bottom: 0.33em; } + +label input { display: block; width: 30em; } diff --git a/html/code/skillsharing/public/skillsharing_client.js b/html/code/skillsharing/public/skillsharing_client.js new file mode 100644 index 000000000..e457d2490 --- /dev/null +++ b/html/code/skillsharing/public/skillsharing_client.js @@ -0,0 +1,178 @@ +function handleAction(state, action) { + if (action.type == "setUser") { + localStorage.setItem("userName", action.user); + return {...state, user: action.user}; + } else if (action.type == "setTalks") { + return {...state, talks: action.talks}; + } else if (action.type == "newTalk") { + fetchOK(talkURL(action.title), { + method: "PUT", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({ + presenter: state.user, + summary: action.summary + }) + }).catch(reportError); + } else if (action.type == "deleteTalk") { + fetchOK(talkURL(action.talk), {method: "DELETE"}) + .catch(reportError); + } else if (action.type == "newComment") { + fetchOK(talkURL(action.talk) + "/comments", { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({ + author: state.user, + message: action.message + }) + }).catch(reportError); + } + return state; +} + +function fetchOK(url, options) { + return fetch(url, options).then(response => { + if (response.status < 400) return response; + else throw new Error(response.statusText); + }); +} + +function talkURL(title) { + return "talks/" + encodeURIComponent(title); +} + +function reportError(error) { + alert(String(error)); +} + +function renderUserField(name, dispatch) { + return elt("label", {}, "Your name: ", elt("input", { + type: "text", + value: name, + onchange(event) { + dispatch({type: "setUser", user: event.target.value}); + } + })); +} + +function elt(type, props, ...children) { + let dom = document.createElement(type); + if (props) Object.assign(dom, props); + for (let child of children) { + if (typeof child != "string") dom.appendChild(child); + else dom.appendChild(document.createTextNode(child)); + } + return dom; +} + +function renderTalk(talk, dispatch) { + return elt( + "section", {className: "talk"}, + elt("h2", null, talk.title, " ", elt("button", { + type: "button", + onclick() { + dispatch({type: "deleteTalk", talk: talk.title}); + } + }, "Delete")), + elt("div", null, "by ", + elt("strong", null, talk.presenter)), + elt("p", null, talk.summary), + ...talk.comments.map(renderComment), + elt("form", { + onsubmit(event) { + event.preventDefault(); + let form = event.target; + dispatch({type: "newComment", + talk: talk.title, + message: form.elements.comment.value}); + form.reset(); + } + }, elt("input", {type: "text", name: "comment"}), " ", + elt("button", {type: "submit"}, "Add comment"))); +} + +function renderComment(comment) { + return elt("p", {className: "comment"}, + elt("strong", null, comment.author), + ": ", comment.message); +} + +function renderTalkForm(dispatch) { + let title = elt("input", {type: "text"}); + let summary = elt("input", {type: "text"}); + return elt("form", { + onsubmit(event) { + event.preventDefault(); + dispatch({type: "newTalk", + title: title.value, + summary: summary.value}); + event.target.reset(); + } + }, elt("h3", null, "Submit a Talk"), + elt("label", null, "Title: ", title), + elt("label", null, "Summary: ", summary), + elt("button", {type: "submit"}, "Submit")); +} + +async function pollTalks(update) { + let tag = undefined; + for (;;) { + let response; + try { + response = await fetchOK("/talks", { + headers: tag && {"If-None-Match": tag, + "Prefer": "wait=90"} + }); + } catch (e) { + console.log("Request failed: " + e); + await new Promise(resolve => setTimeout(resolve, 500)); + continue; + } + if (response.status == 304) continue; + tag = response.headers.get("ETag"); + update(await response.json()); + } +} + +var SkillShareApp = class SkillShareApp { + constructor(state, dispatch) { + this.dispatch = dispatch; + this.talkDOM = elt("div", {className: "talks"}); + this.dom = elt("div", null, + renderUserField(state.user, dispatch), + this.talkDOM, + renderTalkForm(dispatch)); + this.syncState(state); + } + + syncState(state) { + if (state.talks != this.talks) { + this.talkDOM.textContent = ""; + for (let talk of state.talks) { + this.talkDOM.appendChild( + renderTalk(talk, this.dispatch)); + } + this.talks = state.talks; + } + } +} + +function runApp() { + let user = localStorage.getItem("userName") || "Anon"; + let state, app; + function dispatch(action) { + state = handleAction(state, action); + app.syncState(state); + } + + pollTalks(talks => { + if (!app) { + state = {user, talks}; + app = new SkillShareApp(state, dispatch); + document.body.appendChild(app.dom); + } else { + dispatch({type: "setTalks", talks}); + } + }).catch(reportError); +} + +runApp(); diff --git a/html/code/skillsharing/router.mjs b/html/code/skillsharing/router.mjs new file mode 100644 index 000000000..d9164651c --- /dev/null +++ b/html/code/skillsharing/router.mjs @@ -0,0 +1,17 @@ +export class Router { + constructor() { + this.routes = []; + } + add(method, url, handler) { + this.routes.push({method, url, handler}); + } + async resolve(request, context) { + let {pathname} = new URL(request.url, "http://d"); + for (let {method, url, handler} of this.routes) { + let match = url.exec(pathname); + if (!match || request.method != method) continue; + let parts = match.slice(1).map(decodeURIComponent); + return handler(context, ...parts, request); + } + } +} diff --git a/html/code/skillsharing/skillsharing_server.mjs b/html/code/skillsharing/skillsharing_server.mjs new file mode 100644 index 000000000..eb90b6975 --- /dev/null +++ b/html/code/skillsharing/skillsharing_server.mjs @@ -0,0 +1,146 @@ +import {createServer} from "node:http"; +import serveStatic from "serve-static"; + +function notFound(request, response) { + response.writeHead(404, "Not found"); + response.end("

      Not found

      "); +} + +class SkillShareServer { + constructor(talks) { + this.talks = talks; + this.version = 0; + this.waiting = []; + + let fileServer = serveStatic("./public"); + this.server = createServer((request, response) => { + serveFromRouter(this, request, response, () => { + fileServer(request, response, + () => notFound(request, response)); + }); + }); + } + start(port) { + this.server.listen(port); + } + stop() { + this.server.close(); + } +} + +import {Router} from "./router.mjs"; + +const router = new Router(); +const defaultHeaders = {"Content-Type": "text/plain"}; + +async function serveFromRouter(server, request, + response, next) { + let resolved = await router.resolve(request, server) + .catch(error => { + if (error.status != null) return error; + return {body: String(err), status: 500}; + }); + if (!resolved) return next(); + let {body, status = 200, headers = defaultHeaders} = + await resolved; + response.writeHead(status, headers); + response.end(body); +} + +const talkPath = /^\/talks\/([^\/]+)$/; + +router.add("GET", talkPath, async (server, title) => { + if (Object.hasOwn(server.talks, title)) { + return {body: JSON.stringify(server.talks[title]), + headers: {"Content-Type": "application/json"}}; + } else { + return {status: 404, body: `No talk '${title}' found`}; + } +}); + +router.add("DELETE", talkPath, async (server, title) => { + if (Object.hasOwn(server.talks, title)) { + delete server.talks[title]; + server.updated(); + } + return {status: 204}; +}); + +import {json as readJSON} from "node:stream/consumers"; + +router.add("PUT", talkPath, + async (server, title, request) => { + let talk = await readJSON(request); + if (!talk || + typeof talk.presenter != "string" || + typeof talk.summary != "string") { + return {status: 400, body: "Bad talk data"}; + } + server.talks[title] = { + title, + presenter: talk.presenter, + summary: talk.summary, + comments: [] + }; + server.updated(); + return {status: 204}; +}); + +router.add("POST", /^\/talks\/([^\/]+)\/comments$/, + async (server, title, request) => { + let comment = await readJSON(request); + if (!comment || + typeof comment.author != "string" || + typeof comment.message != "string") { + return {status: 400, body: "Bad comment data"}; + } else if (Object.hasOwn(server.talks, title)) { + server.talks[title].comments.push(comment); + server.updated(); + return {status: 204}; + } else { + return {status: 404, body: `No talk '${title}' found`}; + } +}); + +SkillShareServer.prototype.talkResponse = function() { + let talks = Object.keys(this.talks) + .map(title => this.talks[title]); + return { + body: JSON.stringify(talks), + headers: {"Content-Type": "application/json", + "ETag": `"${this.version}"`, + "Cache-Control": "no-store"} + }; +}; + +router.add("GET", /^\/talks$/, async (server, request) => { + let tag = /"(.*)"/.exec(request.headers["if-none-match"]); + let wait = /\bwait=(\d+)/.exec(request.headers["prefer"]); + if (!tag || tag[1] != server.version) { + return server.talkResponse(); + } else if (!wait) { + return {status: 304}; + } else { + return server.waitForChanges(Number(wait[1])); + } +}); + +SkillShareServer.prototype.waitForChanges = function(time) { + return new Promise(resolve => { + this.waiting.push(resolve); + setTimeout(() => { + if (!this.waiting.includes(resolve)) return; + this.waiting = this.waiting.filter(r => r != resolve); + resolve({status: 304}); + }, time * 1000); + }); +}; + +SkillShareServer.prototype.updated = function() { + this.version++; + let response = this.talkResponse(); + this.waiting.forEach(resolve => resolve(response)); + this.waiting = []; +}; + +new SkillShareServer({}).start(8000); diff --git a/html/code/solutions/02_1_looping_a_triangle.js b/html/code/solutions/02_1_looping_a_triangle.js new file mode 100644 index 000000000..2a7ba5f15 --- /dev/null +++ b/html/code/solutions/02_1_looping_a_triangle.js @@ -0,0 +1,2 @@ +for (let line = "#"; line.length < 8; line += "#") + console.log(line); diff --git a/html/code/solutions/02_2_fizzbuzz.js b/html/code/solutions/02_2_fizzbuzz.js new file mode 100644 index 000000000..67138a2a3 --- /dev/null +++ b/html/code/solutions/02_2_fizzbuzz.js @@ -0,0 +1,6 @@ +for (let n = 1; n <= 100; n++) { + let output = ""; + if (n % 3 == 0) output += "Fizz"; + if (n % 5 == 0) output += "Buzz"; + console.log(output || n); +} diff --git a/html/code/solutions/02_3_chessboard.js b/html/code/solutions/02_3_chessboard.js new file mode 100644 index 000000000..0d70bac9b --- /dev/null +++ b/html/code/solutions/02_3_chessboard.js @@ -0,0 +1,16 @@ +let size = 8; + +let board = ""; + +for (let y = 0; y < size; y++) { + for (let x = 0; x < size; x++) { + if ((x + y) % 2 == 0) { + board += " "; + } else { + board += "#"; + } + } + board += "\n"; +} + +console.log(board); diff --git a/html/code/solutions/03_1_minimum.js b/html/code/solutions/03_1_minimum.js new file mode 100644 index 000000000..6954b43a9 --- /dev/null +++ b/html/code/solutions/03_1_minimum.js @@ -0,0 +1,9 @@ +function min(a, b) { + if (a < b) return a; + else return b; +} + +console.log(min(0, 10)); +// → 0 +console.log(min(0, -10)); +// → -10 diff --git a/html/code/solutions/03_2_recursion.js b/html/code/solutions/03_2_recursion.js new file mode 100644 index 000000000..db94f41e4 --- /dev/null +++ b/html/code/solutions/03_2_recursion.js @@ -0,0 +1,13 @@ +function isEven(n) { + if (n == 0) return true; + else if (n == 1) return false; + else if (n < 0) return isEven(-n); + else return isEven(n - 2); +} + +console.log(isEven(50)); +// → true +console.log(isEven(75)); +// → false +console.log(isEven(-1)); +// → false diff --git a/html/code/solutions/03_3_bean_counting.js b/html/code/solutions/03_3_bean_counting.js new file mode 100644 index 000000000..02b04cb86 --- /dev/null +++ b/html/code/solutions/03_3_bean_counting.js @@ -0,0 +1,18 @@ +function countChar(string, ch) { + let counted = 0; + for (let i = 0; i < string.length; i++) { + if (string[i] == ch) { + counted += 1; + } + } + return counted; +} + +function countBs(string) { + return countChar(string, "B"); +} + +console.log(countBs("BBC")); +// → 2 +console.log(countChar("kakkerlak", "k")); +// → 4 diff --git a/html/code/solutions/04_1_the_sum_of_a_range.js b/html/code/solutions/04_1_the_sum_of_a_range.js new file mode 100644 index 000000000..d5b502990 --- /dev/null +++ b/html/code/solutions/04_1_the_sum_of_a_range.js @@ -0,0 +1,25 @@ +function range(start, end, step = start < end ? 1 : -1) { + let array = []; + + if (step > 0) { + for (let i = start; i <= end; i += step) array.push(i); + } else { + for (let i = start; i >= end; i += step) array.push(i); + } + return array; +} + +function sum(array) { + let total = 0; + for (let value of array) { + total += value; + } + return total; +} + +console.log(range(1, 10)) +// → [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +console.log(range(5, 2, -1)); +// → [5, 4, 3, 2] +console.log(sum(range(1, 10))); +// → 55 diff --git a/html/code/solutions/04_2_reversing_an_array.js b/html/code/solutions/04_2_reversing_an_array.js new file mode 100644 index 000000000..442dde8af --- /dev/null +++ b/html/code/solutions/04_2_reversing_an_array.js @@ -0,0 +1,23 @@ +function reverseArray(array) { + let output = []; + for (let i = array.length - 1; i >= 0; i--) { + output.push(array[i]); + } + return output; +} + +function reverseArrayInPlace(array) { + for (let i = 0; i < Math.floor(array.length / 2); i++) { + let old = array[i]; + array[i] = array[array.length - 1 - i]; + array[array.length - 1 - i] = old; + } + return array; +} + +console.log(reverseArray(["A", "B", "C"])); +// → ["C", "B", "A"]; +let arrayValue = [1, 2, 3, 4, 5]; +reverseArrayInPlace(arrayValue); +console.log(arrayValue); +// → [5, 4, 3, 2, 1] diff --git a/html/code/solutions/04_3_a_list.js b/html/code/solutions/04_3_a_list.js new file mode 100644 index 000000000..bf93ac075 --- /dev/null +++ b/html/code/solutions/04_3_a_list.js @@ -0,0 +1,34 @@ +function arrayToList(array) { + let list = null; + for (let i = array.length - 1; i >= 0; i--) { + list = {value: array[i], rest: list}; + } + return list; +} + +function listToArray(list) { + let array = []; + for (let node = list; node; node = node.rest) { + array.push(node.value); + } + return array; +} + +function prepend(value, list) { + return {value, rest: list}; +} + +function nth(list, n) { + if (!list) return undefined; + else if (n == 0) return list.value; + else return nth(list.rest, n - 1); +} + +console.log(arrayToList([10, 20])); +// → {value: 10, rest: {value: 20, rest: null}} +console.log(listToArray(arrayToList([10, 20, 30]))); +// → [10, 20, 30] +console.log(prepend(10, prepend(20, null))); +// → {value: 10, rest: {value: 20, rest: null}} +console.log(nth(arrayToList([10, 20, 30]), 1)); +// → 20 diff --git a/html/code/solutions/04_4_deep_comparison.js b/html/code/solutions/04_4_deep_comparison.js new file mode 100644 index 000000000..bd3fde9b1 --- /dev/null +++ b/html/code/solutions/04_4_deep_comparison.js @@ -0,0 +1,24 @@ +function deepEqual(a, b) { + if (a === b) return true; + + if (a == null || typeof a != "object" || + b == null || typeof b != "object") return false; + + let keysA = Object.keys(a), keysB = Object.keys(b); + + if (keysA.length != keysB.length) return false; + + for (let key of keysA) { + if (!keysB.includes(key) || !deepEqual(a[key], b[key])) return false; + } + + return true; +} + +let obj = {here: {is: "an"}, object: 2}; +console.log(deepEqual(obj, obj)); +// → true +console.log(deepEqual(obj, {here: 1, object: 2})); +// → false +console.log(deepEqual(obj, {here: {is: "an"}, object: 2})); +// → true diff --git a/html/code/solutions/05_1_flattening.js b/html/code/solutions/05_1_flattening.js new file mode 100644 index 000000000..fbd6b0fed --- /dev/null +++ b/html/code/solutions/05_1_flattening.js @@ -0,0 +1,4 @@ +let arrays = [[1, 2, 3], [4, 5], [6]]; + +console.log(arrays.reduce((flat, current) => flat.concat(current), [])); +// → [1, 2, 3, 4, 5, 6] diff --git a/html/code/solutions/05_2_your_own_loop.js b/html/code/solutions/05_2_your_own_loop.js new file mode 100644 index 000000000..55a715c43 --- /dev/null +++ b/html/code/solutions/05_2_your_own_loop.js @@ -0,0 +1,10 @@ +function loop(start, test, update, body) { + for (let value = start; test(value); value = update(value)) { + body(value); + } +} + +loop(3, n => n > 0, n => n - 1, console.log); +// → 3 +// → 2 +// → 1 diff --git a/html/code/solutions/05_3_everything.js b/html/code/solutions/05_3_everything.js new file mode 100644 index 000000000..08e0e533a --- /dev/null +++ b/html/code/solutions/05_3_everything.js @@ -0,0 +1,17 @@ +function every(array, predicate) { + for (let element of array) { + if (!predicate(element)) return false; + } + return true; +} + +function every2(array, predicate) { + return !array.some(element => !predicate(element)); +} + +console.log(every([1, 3, 5], n => n < 10)); +// → true +console.log(every([2, 4, 16], n => n < 10)); +// → false +console.log(every([], n => n < 10)); +// → true diff --git a/html/code/solutions/05_4_dominant_writing_direction.js b/html/code/solutions/05_4_dominant_writing_direction.js new file mode 100644 index 000000000..1b5494981 --- /dev/null +++ b/html/code/solutions/05_4_dominant_writing_direction.js @@ -0,0 +1,15 @@ +function dominantDirection(text) { + let counted = countBy(text, char => { + let script = characterScript(char.codePointAt(0)); + return script ? script.direction : "none"; + }).filter(({name}) => name != "none"); + + if (counted.length == 0) return "ltr"; + + return counted.reduce((a, b) => a.count > b.count ? a : b).name; +} + +console.log(dominantDirection("Hello!")); +// → ltr +console.log(dominantDirection("Hey, مساء الخير")); +// → rtl diff --git a/html/code/solutions/06_1_a_vector_type.js b/html/code/solutions/06_1_a_vector_type.js new file mode 100644 index 000000000..ee676c10e --- /dev/null +++ b/html/code/solutions/06_1_a_vector_type.js @@ -0,0 +1,25 @@ +class Vec { + constructor(x, y) { + this.x = x; + this.y = y; + } + + plus(other) { + return new Vec(this.x + other.x, this.y + other.y); + } + + minus(other) { + return new Vec(this.x - other.x, this.y - other.y); + } + + get length() { + return Math.sqrt(this.x * this.x + this.y * this.y); + } +} + +console.log(new Vec(1, 2).plus(new Vec(2, 3))); +// → Vec{x: 3, y: 5} +console.log(new Vec(1, 2).minus(new Vec(2, 3))); +// → Vec{x: -1, y: -1} +console.log(new Vec(3, 4).length); +// → 5 diff --git a/html/code/solutions/06_2_groups.js b/html/code/solutions/06_2_groups.js new file mode 100644 index 000000000..2aa1f952a --- /dev/null +++ b/html/code/solutions/06_2_groups.js @@ -0,0 +1,35 @@ +class Group { + #members = []; + + add(value) { + if (!this.has(value)) { + this.#members.push(value); + } + } + + delete(value) { + this.#members = this.#members.filter(v => v !== value); + } + + has(value) { + return this.#members.includes(value); + } + + static from(collection) { + let group = new Group; + for (let value of collection) { + group.add(value); + } + return group; + } +} + +let group = Group.from([10, 20]); +console.log(group.has(10)); +// → true +console.log(group.has(30)); +// → false +group.add(10); +group.delete(10); +console.log(group.has(10)); +// → false diff --git a/html/code/solutions/06_3_iterable_groups.js b/html/code/solutions/06_3_iterable_groups.js new file mode 100644 index 000000000..557a35fb2 --- /dev/null +++ b/html/code/solutions/06_3_iterable_groups.js @@ -0,0 +1,57 @@ +class Group { + #members = []; + + add(value) { + if (!this.has(value)) { + this.#members.push(value); + } + } + + delete(value) { + this.#members = this.#members.filter(v => v !== value); + } + + has(value) { + return this.#members.includes(value); + } + + static from(collection) { + let group = new Group; + for (let value of collection) { + group.add(value); + } + return group; + } + + [Symbol.iterator]() { + return new GroupIterator(this.#members); + } +} + +class GroupIterator { + #members; + #position; + + constructor(members) { + this.#members = members; + this.#position = 0; + } + + next() { + if (this.#position >= this.#members.length) { + return {done: true}; + } else { + let result = {value: this.#members[this.#position], + done: false}; + this.#position++; + return result; + } + } +} + +for (let value of Group.from(["a", "b", "c"])) { + console.log(value); +} +// → a +// → b +// → c diff --git a/html/code/solutions/06_4_borrowing_a_method.js b/html/code/solutions/06_4_borrowing_a_method.js new file mode 100644 index 000000000..29543894e --- /dev/null +++ b/html/code/solutions/06_4_borrowing_a_method.js @@ -0,0 +1,4 @@ +let map = {one: true, two: true, hasOwnProperty: true}; + +console.log(Object.prototype.hasOwnProperty.call(map, "one")); +// → true diff --git a/html/code/solutions/07_1_measuring_a_robot.js b/html/code/solutions/07_1_measuring_a_robot.js new file mode 100644 index 000000000..dc95bbcee --- /dev/null +++ b/html/code/solutions/07_1_measuring_a_robot.js @@ -0,0 +1,21 @@ +function countSteps(state, robot, memory) { + for (let steps = 0;; steps++) { + if (state.parcels.length == 0) return steps; + let action = robot(state, memory); + state = state.move(action.direction); + memory = action.memory; + } +} + +function compareRobots(robot1, memory1, robot2, memory2) { + let total1 = 0, total2 = 0; + for (let i = 0; i < 100; i++) { + let state = VillageState.random(); + total1 += countSteps(state, robot1, memory1); + total2 += countSteps(state, robot2, memory2); + } + console.log(`Robot 1 needed ${total1 / 100} steps per task`) + console.log(`Robot 2 needed ${total2 / 100}`) +} + +compareRobots(routeRobot, [], goalOrientedRobot, []); diff --git a/html/code/solutions/07_2_robot_efficiency.js b/html/code/solutions/07_2_robot_efficiency.js new file mode 100644 index 000000000..5d2184aa1 --- /dev/null +++ b/html/code/solutions/07_2_robot_efficiency.js @@ -0,0 +1,26 @@ +function lazyRobot({place, parcels}, route) { + if (route.length == 0) { + // Describe a route for every parcel + let routes = parcels.map(parcel => { + if (parcel.place != place) { + return {route: findRoute(roadGraph, place, parcel.place), + pickUp: true}; + } else { + return {route: findRoute(roadGraph, place, parcel.address), + pickUp: false}; + } + }); + + // This determines the precedence a route gets when choosing. + // Route length counts negatively, routes that pick up a package + // get a small bonus. + function score({route, pickUp}) { + return (pickUp ? 0.5 : 0) - route.length; + } + route = routes.reduce((a, b) => score(a) > score(b) ? a : b).route; + } + + return {direction: route[0], memory: route.slice(1)}; +} + +runRobotAnimation(VillageState.random(), lazyRobot, []); diff --git a/html/code/solutions/07_3_persistent_group.js b/html/code/solutions/07_3_persistent_group.js new file mode 100644 index 000000000..31ec76c80 --- /dev/null +++ b/html/code/solutions/07_3_persistent_group.js @@ -0,0 +1,33 @@ +class PGroup { + #members; + constructor(members) { + this.#members = members; + } + + add(value) { + if (this.has(value)) return this; + return new PGroup(this.#members.concat([value])); + } + + delete(value) { + if (!this.has(value)) return this; + return new PGroup(this.#members.filter(m => m !== value)); + } + + has(value) { + return this.#members.includes(value); + } + + static empty = new PGroup([]); +} + +let a = PGroup.empty.add("a"); +let ab = a.add("b"); +let b = ab.delete("a"); + +console.log(b.has("b")); +// → true +console.log(a.has("b")); +// → false +console.log(b.has("a")); +// → false diff --git a/html/code/solutions/08_1_retry.js b/html/code/solutions/08_1_retry.js new file mode 100644 index 000000000..f2a7ae10c --- /dev/null +++ b/html/code/solutions/08_1_retry.js @@ -0,0 +1,23 @@ +class MultiplicatorUnitFailure extends Error {} + +function primitiveMultiply(a, b) { + if (Math.random() < 0.2) { + return a * b; + } else { + throw new MultiplicatorUnitFailure("Klunk"); + } +} + +function reliableMultiply(a, b) { + for (;;) { + try { + return primitiveMultiply(a, b); + } catch (e) { + if (!(e instanceof MultiplicatorUnitFailure)) + throw e; + } + } +} + +console.log(reliableMultiply(8, 8)); +// → 64 diff --git a/html/code/solutions/08_2_the_locked_box.js b/html/code/solutions/08_2_the_locked_box.js new file mode 100644 index 000000000..b48665c01 --- /dev/null +++ b/html/code/solutions/08_2_the_locked_box.js @@ -0,0 +1,36 @@ +const box = new class { + locked = true; + #content = []; + + unlock() { this.locked = false; } + lock() { this.locked = true; } + get content() { + if (this.locked) throw new Error("Locked!"); + return this.#content; + } +}; + +function withBoxUnlocked(body) { + let locked = box.locked; + if (locked) box.unlock(); + try { + return body(); + } finally { + if (locked) box.lock(); + } +} + +withBoxUnlocked(() => { + box.content.push("gold piece"); +}); + +try { + withBoxUnlocked(() => { + throw new Error("Pirates on the horizon! Abort!"); + }); +} catch (e) { + console.log("Error raised:", e); +} + +console.log(box.locked); +// → true diff --git a/html/code/solutions/09_1_regexp_golf.js b/html/code/solutions/09_1_regexp_golf.js new file mode 100644 index 000000000..ad84ccd4f --- /dev/null +++ b/html/code/solutions/09_1_regexp_golf.js @@ -0,0 +1,41 @@ +// Fill in the regular expressions + +verify(/ca[rt]/, + ["my car", "bad cats"], + ["camper", "high art"]); + +verify(/pr?op/, + ["pop culture", "mad props"], + ["plop", "prrrop"]); + +verify(/ferr(et|y|ari)/, + ["ferret", "ferry", "ferrari"], + ["ferrum", "transfer A"]); + +verify(/ious($|\P{L})/u, + ["how delicious", "spacious room"], + ["ruinous", "consciousness"]); + +verify(/\s[.,:;]/, + ["bad punctuation ."], + ["escape the dot"]); + +verify(/\p{L}{7}/u, + ["Siebentausenddreihundertzweiundzwanzig"], + ["no", "three small words"]); + +verify(/(^|\P{L})[^\P{L}e]+($|\P{L})/ui, + ["red platypus", "wobbling nest"], + ["earth bed", "bedrøvet abe", "BEET"]); + + +function verify(regexp, yes, no) { + // Ignore unfinished exercises + if (regexp.source == "...") return; + for (let str of yes) if (!regexp.test(str)) { + console.log(`Failure to match '${str}'`); + } + for (let str of no) if (regexp.test(str)) { + console.log(`Unexpected match for '${str}'`); + } +} diff --git a/html/code/solutions/09_2_quoting_style.js b/html/code/solutions/09_2_quoting_style.js new file mode 100644 index 000000000..dcae1cddf --- /dev/null +++ b/html/code/solutions/09_2_quoting_style.js @@ -0,0 +1,5 @@ +let text = "'I'm the cook,' he said, 'it's my job.'"; + +console.log(text.replace(/(^|\P{L})'|'(\P{L}|$)/gu, '$1"$2')); +// → "I'm the cook," he said, "it's my job." + diff --git a/html/code/solutions/09_3_numbers_again.js b/html/code/solutions/09_3_numbers_again.js new file mode 100644 index 000000000..34691f449 --- /dev/null +++ b/html/code/solutions/09_3_numbers_again.js @@ -0,0 +1,16 @@ +// Fill in this regular expression. +let number = /^[+\-]?(\d+(\.\d*)?|\.\d+)([eE][+\-]?\d+)?$/; + +// Tests: +for (let str of ["1", "-1", "+15", "1.55", ".5", "5.", + "1.3e2", "1E-4", "1e+12"]) { + if (!number.test(str)) { + console.log(`Failed to match '${str}'`); + } +} +for (let str of ["1a", "+-1", "1.2.3", "1+1", "1e4.5", + ".5.", "1f5", "."]) { + if (number.test(str)) { + console.log(`Incorrectly accepted '${str}'`); + } +} diff --git a/html/code/solutions/10_2_roads_module.js b/html/code/solutions/10_2_roads_module.js new file mode 100644 index 000000000..af405b799 --- /dev/null +++ b/html/code/solutions/10_2_roads_module.js @@ -0,0 +1,13 @@ +import {buildGraph} from "./graph"; + +const roads = [ + "Alice's House-Bob's House", "Alice's House-Cabin", + "Alice's House-Post Office", "Bob's House-Town Hall", + "Daria's House-Ernie's House", "Daria's House-Town Hall", + "Ernie's House-Grete's House", "Grete's House-Farm", + "Grete's House-Shop", "Marketplace-Farm", + "Marketplace-Post Office", "Marketplace-Shop", + "Marketplace-Town Hall", "Shop-Town Hall" +]; + +export const roadGraph = buildGraph(roads.map(r => r.split("-"))); diff --git a/html/code/solutions/11_1_quiet_times.js b/html/code/solutions/11_1_quiet_times.js new file mode 100644 index 000000000..8d20b1fd2 --- /dev/null +++ b/html/code/solutions/11_1_quiet_times.js @@ -0,0 +1,20 @@ +async function activityTable(day) { + let table = []; + for (let i = 0; i < 24; i++) table[i] = 0; + + let logFileList = await textFile("camera_logs.txt"); + for (let filename of logFileList.split("\n")) { + let log = await textFile(filename); + for (let timestamp of log.split("\n")) { + let date = new Date(Number(timestamp)); + if (date.getDay() == day) { + table[date.getHours()]++; + } + } + } + + return table; +} + +activityTable(1) + .then(table => console.log(activityGraph(table))); diff --git a/html/code/solutions/11_1_tracking_the_scalpel.js b/html/code/solutions/11_1_tracking_the_scalpel.js new file mode 100644 index 000000000..cd74b5119 --- /dev/null +++ b/html/code/solutions/11_1_tracking_the_scalpel.js @@ -0,0 +1,23 @@ +async function locateScalpel(nest) { + let current = nest.name; + for (;;) { + let next = await anyStorage(nest, current, "scalpel"); + if (next == current) return current; + current = next; + } +} + +function locateScalpel2(nest) { + function loop(current) { + return anyStorage(nest, current, "scalpel").then(next => { + if (next == current) return current; + else return loop(next); + }); + } + return loop(nest.name); +} + +locateScalpel(bigOak).then(console.log); +// → Butcher's Shop +locateScalpel2(bigOak).then(console.log); +// → Butcher's Shop diff --git a/html/code/solutions/11_2_real_promises.js b/html/code/solutions/11_2_real_promises.js new file mode 100644 index 000000000..1c263543c --- /dev/null +++ b/html/code/solutions/11_2_real_promises.js @@ -0,0 +1,20 @@ +function activityTable(day) { + let table = []; + for (let i = 0; i < 24; i++) table[i] = 0; + + return textFile("camera_logs.txt").then(files => { + return Promise.all(files.split("\n").map(name => { + return textFile(name).then(log => { + for (let timestamp of log.split("\n")) { + let date = new Date(Number(timestamp)); + if (date.getDay() == day) { + table[date.getHours()]++; + } + } + }); + })); + }).then(() => table); +} + +activityTable(6) + .then(table => console.log(activityGraph(table))); diff --git a/html/code/solutions/11_3_building_promiseall.js b/html/code/solutions/11_3_building_promiseall.js new file mode 100644 index 000000000..9e8e1465e --- /dev/null +++ b/html/code/solutions/11_3_building_promiseall.js @@ -0,0 +1,34 @@ +function Promise_all(promises) { + return new Promise((resolve, reject) => { + let results = []; + let pending = promises.length; + for (let i = 0; i < promises.length; i++) { + promises[i].then(result => { + results[i] = result; + pending--; + if (pending == 0) resolve(results); + }).catch(reject); + } + if (promises.length == 0) resolve(results); + }); +} + +// Test code. +Promise_all([]).then(array => { + console.log("This should be []:", array); +}); +function soon(val) { + return new Promise(resolve => { + setTimeout(() => resolve(val), Math.random() * 500); + }); +} +Promise_all([soon(1), soon(2), soon(3)]).then(array => { + console.log("This should be [1, 2, 3]:", array); +}); +Promise_all([soon(1), Promise.reject("X"), soon(3)]).then(array => { + console.log("We should not get here"); +}).catch(error => { + if (error != "X") { + console.log("Unexpected failure:", error); + } +}); diff --git a/html/code/solutions/12_1_arrays.js b/html/code/solutions/12_1_arrays.js new file mode 100644 index 000000000..e857c914b --- /dev/null +++ b/html/code/solutions/12_1_arrays.js @@ -0,0 +1,17 @@ +topScope.array = (...values) => values; + +topScope.length = array => array.length; + +topScope.element = (array, i) => array[i]; + +run(` +do(define(sum, fun(array, + do(define(i, 0), + define(sum, 0), + while(<(i, length(array)), + do(define(sum, +(sum, element(array, i))), + define(i, +(i, 1)))), + sum))), + print(sum(array(1, 2, 3)))) +`); +// → 6 diff --git a/html/code/solutions/12_3_comments.js b/html/code/solutions/12_3_comments.js new file mode 100644 index 000000000..7afc21a9e --- /dev/null +++ b/html/code/solutions/12_3_comments.js @@ -0,0 +1,12 @@ +function skipSpace(string) { + let skippable = string.match(/^(\s|#.*)*/); + return string.slice(skippable[0].length); +} + +console.log(parse("# hello\nx")); +// → {type: "word", name: "x"} + +console.log(parse("a # one\n # two\n()")); +// → {type: "apply", +// operator: {type: "word", name: "a"}, +// args: []} diff --git a/html/code/solutions/12_4_fixing_scope.js b/html/code/solutions/12_4_fixing_scope.js new file mode 100644 index 000000000..fb75ba9f2 --- /dev/null +++ b/html/code/solutions/12_4_fixing_scope.js @@ -0,0 +1,25 @@ +specialForms.set = (args, env) => { + if (args.length != 2 || args[0].type != "word") { + throw new SyntaxError("Bad use of set"); + } + let varName = args[0].name; + let value = evaluate(args[1], env); + + for (let scope = env; scope; scope = Object.getPrototypeOf(scope)) { + if (Object.hasOwn(scope, varName)) { + scope[varName] = value; + return value; + } + } + throw new ReferenceError(`Setting undefined variable ${varName}`); +}; + +run(` +do(define(x, 4), + define(setx, fun(val, set(x, val))), + setx(50), + print(x)) +`); +// → 50 +run(`set(quux, true)`); +// → Some kind of ReferenceError diff --git a/html/code/solutions/14_1_build_a_table.html b/html/code/solutions/14_1_build_a_table.html new file mode 100644 index 000000000..9b74c1ed0 --- /dev/null +++ b/html/code/solutions/14_1_build_a_table.html @@ -0,0 +1,49 @@ + + + +

      Mountains

      + +
      + + diff --git a/html/code/solutions/14_2_elements_by_tag_name.html b/html/code/solutions/14_2_elements_by_tag_name.html new file mode 100644 index 000000000..45e9dbc3b --- /dev/null +++ b/html/code/solutions/14_2_elements_by_tag_name.html @@ -0,0 +1,33 @@ + + +

      Heading with a span element.

      +

      A paragraph with one, two + spans.

      + + diff --git a/html/code/solutions/14_3_the_cats_hat.html b/html/code/solutions/14_3_the_cats_hat.html new file mode 100644 index 000000000..92d7444a4 --- /dev/null +++ b/html/code/solutions/14_3_the_cats_hat.html @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/html/code/solutions/15_1_balloon.html b/html/code/solutions/15_1_balloon.html new file mode 100644 index 000000000..8adfeb9a1 --- /dev/null +++ b/html/code/solutions/15_1_balloon.html @@ -0,0 +1,29 @@ + + +

      🎈

      + + diff --git a/html/code/solutions/15_2_mouse_trail.html b/html/code/solutions/15_2_mouse_trail.html new file mode 100644 index 000000000..1e9e48855 --- /dev/null +++ b/html/code/solutions/15_2_mouse_trail.html @@ -0,0 +1,33 @@ + + + + + + + diff --git a/html/code/solutions/15_3_tabs.html b/html/code/solutions/15_3_tabs.html new file mode 100644 index 000000000..04edba1d2 --- /dev/null +++ b/html/code/solutions/15_3_tabs.html @@ -0,0 +1,33 @@ + + + +
      Tab one
      +
      Tab two
      +
      Tab three
      +
      + diff --git a/html/code/solutions/16_1_game_over.html b/html/code/solutions/16_1_game_over.html new file mode 100644 index 000000000..5b6579300 --- /dev/null +++ b/html/code/solutions/16_1_game_over.html @@ -0,0 +1,29 @@ + + + + + + + + + + + diff --git a/html/code/solutions/16_2_pausing_the_game.html b/html/code/solutions/16_2_pausing_the_game.html new file mode 100644 index 000000000..756261f33 --- /dev/null +++ b/html/code/solutions/16_2_pausing_the_game.html @@ -0,0 +1,93 @@ + + + + + + + + + + + diff --git a/html/code/solutions/16_3_a_monster.html b/html/code/solutions/16_3_a_monster.html new file mode 100644 index 000000000..a9ef810aa --- /dev/null +++ b/html/code/solutions/16_3_a_monster.html @@ -0,0 +1,62 @@ + + + + + + + + + + + + + diff --git a/html/code/solutions/17_1_shapes.html b/html/code/solutions/17_1_shapes.html new file mode 100644 index 000000000..f5d6fb481 --- /dev/null +++ b/html/code/solutions/17_1_shapes.html @@ -0,0 +1,66 @@ + + + + diff --git a/html/code/solutions/17_2_the_pie_chart.html b/html/code/solutions/17_2_the_pie_chart.html new file mode 100644 index 000000000..c3ac4e408 --- /dev/null +++ b/html/code/solutions/17_2_the_pie_chart.html @@ -0,0 +1,40 @@ + + + + + + + diff --git a/html/code/solutions/17_3_a_bouncing_ball.html b/html/code/solutions/17_3_a_bouncing_ball.html new file mode 100644 index 000000000..afd954165 --- /dev/null +++ b/html/code/solutions/17_3_a_bouncing_ball.html @@ -0,0 +1,36 @@ + + + + diff --git a/html/code/solutions/18_1_content_negotiation.js b/html/code/solutions/18_1_content_negotiation.js new file mode 100644 index 000000000..bd203e052 --- /dev/null +++ b/html/code/solutions/18_1_content_negotiation.js @@ -0,0 +1,14 @@ +const url = "https://eloquentjavascript.net/author"; +const types = ["text/plain", + "text/html", + "application/json", + "application/rainbows+unicorns"]; + +async function showTypes() { + for (let type of types) { + let resp = await fetch(url, {headers: {accept: type}}); + console.log(`${type}: ${await resp.text()}\n`); + } +} + +showTypes(); diff --git a/html/code/solutions/18_2_a_javascript_workbench.html b/html/code/solutions/18_2_a_javascript_workbench.html new file mode 100644 index 000000000..5e087fd76 --- /dev/null +++ b/html/code/solutions/18_2_a_javascript_workbench.html @@ -0,0 +1,18 @@ + + + + +
      
      +
      +
      diff --git a/html/code/solutions/18_3_conways_game_of_life.html b/html/code/solutions/18_3_conways_game_of_life.html
      new file mode 100644
      index 000000000..a32165ac5
      --- /dev/null
      +++ b/html/code/solutions/18_3_conways_game_of_life.html
      @@ -0,0 +1,89 @@
      +
      +
      +
      + + + + diff --git a/html/code/solutions/19_1_keyboard_bindings.html b/html/code/solutions/19_1_keyboard_bindings.html new file mode 100644 index 000000000..0e4b84bb5 --- /dev/null +++ b/html/code/solutions/19_1_keyboard_bindings.html @@ -0,0 +1,52 @@ + + + + + +
      + diff --git a/html/code/solutions/19_2_efficient_drawing.html b/html/code/solutions/19_2_efficient_drawing.html new file mode 100644 index 000000000..c0c8b2f5d --- /dev/null +++ b/html/code/solutions/19_2_efficient_drawing.html @@ -0,0 +1,37 @@ + + + + + +
      + diff --git a/html/code/solutions/19_3_circles.html b/html/code/solutions/19_3_circles.html new file mode 100644 index 000000000..4fcab5bf4 --- /dev/null +++ b/html/code/solutions/19_3_circles.html @@ -0,0 +1,34 @@ + + + + + +
      + diff --git a/html/code/solutions/19_4_proper_lines.html b/html/code/solutions/19_4_proper_lines.html new file mode 100644 index 000000000..4cb9e2a39 --- /dev/null +++ b/html/code/solutions/19_4_proper_lines.html @@ -0,0 +1,49 @@ + + + + + +
      + diff --git a/html/code/solutions/20_1_search_tool.mjs b/html/code/solutions/20_1_search_tool.mjs new file mode 100644 index 000000000..2cf38424b --- /dev/null +++ b/html/code/solutions/20_1_search_tool.mjs @@ -0,0 +1,18 @@ +import {statSync, readdirSync, readFileSync} from "node:fs"; + +let searchTerm = new RegExp(process.argv[2]); + +for (let arg of process.argv.slice(3)) { + search(arg); +} + +function search(file) { + let stats = statSync(file); + if (stats.isDirectory()) { + for (let f of readdirSync(file)) { + search(file + "/" + f); + } + } else if (searchTerm.test(readFileSync(file, "utf8"))) { + console.log(file); + } +} diff --git a/html/code/solutions/20_2_directory_creation.mjs b/html/code/solutions/20_2_directory_creation.mjs new file mode 100644 index 000000000..6033643a3 --- /dev/null +++ b/html/code/solutions/20_2_directory_creation.mjs @@ -0,0 +1,18 @@ +// This code won't work on its own, but is also included in the +// code/file_server.js file, which defines the whole system. + +import {mkdir} from "node:fs/promises"; + +methods.MKCOL = async function(request) { + let path = urlPath(request.url); + let stats; + try { + stats = await stat(path); + } catch (error) { + if (error.code != "ENOENT") throw error; + await mkdir(path); + return {status: 204}; + } + if (stats.isDirectory()) return {status: 204}; + else return {status: 400, body: "Not a directory"}; +}; diff --git a/html/code/solutions/20_3_a_public_space_on_the_web.zip b/html/code/solutions/20_3_a_public_space_on_the_web.zip new file mode 100644 index 000000000..a36913788 Binary files /dev/null and b/html/code/solutions/20_3_a_public_space_on_the_web.zip differ diff --git a/html/code/solutions/20_3_a_public_space_on_the_web/index.html b/html/code/solutions/20_3_a_public_space_on_the_web/index.html new file mode 100644 index 000000000..e6ad51515 --- /dev/null +++ b/html/code/solutions/20_3_a_public_space_on_the_web/index.html @@ -0,0 +1,17 @@ + + + +

      A Public Space on the Web

      + +

      This is a self-editing website. Select a file, edit it, and save to +update the website.

      + +

      Files:

      + +

      + +
      + +

      + + diff --git a/html/code/solutions/20_3_a_public_space_on_the_web/other.html b/html/code/solutions/20_3_a_public_space_on_the_web/other.html new file mode 100644 index 000000000..a9b89b115 --- /dev/null +++ b/html/code/solutions/20_3_a_public_space_on_the_web/other.html @@ -0,0 +1,4 @@ + + + +

      This is another file

      diff --git a/html/code/solutions/20_3_a_public_space_on_the_web/public_space.js b/html/code/solutions/20_3_a_public_space_on_the_web/public_space.js new file mode 100644 index 000000000..ee321fc42 --- /dev/null +++ b/html/code/solutions/20_3_a_public_space_on_the_web/public_space.js @@ -0,0 +1,31 @@ +// Get a reference to the DOM nodes we need +let filelist = document.querySelector("#filelist"); +let textarea = document.querySelector("#file"); + +// This loads the initial file list from the server +fetch("/").then(resp => resp.text()).then(files => { + for (let file of files.split("\n")) { + let option = document.createElement("option"); + option.textContent = file; + filelist.appendChild(option); + } + // Now that we have a list of files, make sure the textarea contains + // the currently selected one. + loadCurrentFile(); +}); + +// Fetch a file from the server and put it in the textarea. +function loadCurrentFile() { + fetch(filelist.value).then(resp => resp.text()).then(file => { + textarea.value = file; + }); +} + +filelist.addEventListener("change", loadCurrentFile); + +// Called by the button on the page. Makes a request to save the +// currently selected file. +function saveFile() { + fetch(filelist.value, {method: "PUT", + body: textarea.value}); +} diff --git a/html/code/solutions/21_1_disk_persistence.mjs b/html/code/solutions/21_1_disk_persistence.mjs new file mode 100644 index 000000000..6aa3bcd8e --- /dev/null +++ b/html/code/solutions/21_1_disk_persistence.mjs @@ -0,0 +1,28 @@ +// This isn't a stand-alone file, only a redefinition of a few +// fragments from skillsharing/skillsharing_server.js + +import {readFileSync, writeFile} from "node:fs"; + +const fileName = "./talks.json"; + +SkillShareServer.prototype.updated = function() { + this.version++; + let response = this.talkResponse(); + this.waiting.forEach(resolve => resolve(response)); + this.waiting = []; + + writeFile(fileName, JSON.stringify(this.talks), e => { + if (e) throw e; + }); +}; + +function loadTalks() { + try { + return JSON.parse(readFileSync(fileName, "utf8")); + } catch (e) { + return {}; + } +} + +// The line that starts the server must be changed to +new SkillShareServer(loadTalks()).start(8000); diff --git a/html/code/solutions/21_2_comment_field_resets.mjs b/html/code/solutions/21_2_comment_field_resets.mjs new file mode 100644 index 000000000..ac3cc0cdd --- /dev/null +++ b/html/code/solutions/21_2_comment_field_resets.mjs @@ -0,0 +1,76 @@ +// This isn't a stand-alone file, only a redefinition of the main +// component from skillsharing/public/skillsharing_client.js + +class Talk { + constructor(talk, dispatch) { + this.comments = elt("div"); + this.dom = elt( + "section", {className: "talk"}, + elt("h2", null, talk.title, " ", elt("button", { + type: "button", + onclick: () => dispatch({type: "deleteTalk", + talk: talk.title}) + }, "Delete")), + elt("div", null, "by ", + elt("strong", null, talk.presenter)), + elt("p", null, talk.summary), + this.comments, + elt("form", { + onsubmit(event) { + event.preventDefault(); + let form = event.target; + dispatch({type: "newComment", + talk: talk.title, + message: form.elements.comment.value}); + form.reset(); + } + }, elt("input", {type: "text", name: "comment"}), " ", + elt("button", {type: "submit"}, "Add comment"))); + this.syncState(talk); + } + + syncState(talk) { + this.talk = talk; + this.comments.textContent = ""; + for (let comment of talk.comments) { + this.comments.appendChild(renderComment(comment)); + } + } +} + +class SkillShareApp { + constructor(state, dispatch) { + this.dispatch = dispatch; + this.talkDOM = elt("div", {className: "talks"}); + this.talkMap = Object.create(null); + this.dom = elt("div", null, + renderUserField(state.user, dispatch), + this.talkDOM, + renderTalkForm(dispatch)); + this.syncState(state); + } + + syncState(state) { + if (state.talks == this.talks) return; + this.talks = state.talks; + + for (let talk of state.talks) { + let found = this.talkMap[talk.title]; + if (found && found.talk.presenter == talk.presenter && + found.talk.summary == talk.summary) { + found.syncState(talk); + } else { + if (found) found.dom.remove(); + found = new Talk(talk, this.dispatch); + this.talkMap[talk.title] = found; + this.talkDOM.appendChild(found.dom); + } + } + for (let title of Object.keys(this.talkMap)) { + if (!state.talks.some(talk => talk.title == title)) { + this.talkMap[title].dom.remove(); + delete this.talkMap[title]; + } + } + } +} diff --git a/html/code/solutions/22_1_pathfinding.js b/html/code/solutions/22_1_pathfinding.js new file mode 100644 index 000000000..b9907a859 --- /dev/null +++ b/html/code/solutions/22_1_pathfinding.js @@ -0,0 +1,21 @@ +function findPath(a, b) { + let work = [[a]]; + for (let path of work) { + let end = path[path.length - 1]; + if (end == b) return path; + for (let next of end.edges) { + if (!work.some(path => path[path.length - 1] == next)) { + work.push(path.concat([next])); + } + } + } +} + +let graph = treeGraph(4, 4); +let root = graph[0], leaf = graph[graph.length - 1]; +console.log(findPath(root, leaf).length); +// → 4 + +leaf.connect(root); +console.log(findPath(root, leaf).length); +// → 2 diff --git a/html/code/solutions/22_1_prime_numbers.js b/html/code/solutions/22_1_prime_numbers.js new file mode 100644 index 000000000..ecf67e66b --- /dev/null +++ b/html/code/solutions/22_1_prime_numbers.js @@ -0,0 +1,22 @@ +function* primes() { + for (let n = 2;; n++) { + let skip = false; + for (let d = 2; d < n; d++) { + if (n % d == 0) { + skip = true; + break; + } + } + if (!skip) yield n; + } +} + +function measurePrimes() { + let iter = primes(), t0 = Date.now(); + for (let i = 0; i < 10000; i++) { + iter.next(); + } + console.log(`Took ${Date.now() - t0}ms`); +} + +measurePrimes(); diff --git a/html/code/solutions/22_2_faster_prime_numbers.js b/html/code/solutions/22_2_faster_prime_numbers.js new file mode 100644 index 000000000..bc3f6f37b --- /dev/null +++ b/html/code/solutions/22_2_faster_prime_numbers.js @@ -0,0 +1,28 @@ +function* primes() { + let found = []; + for (let n = 2;; n++) { + let skip = false, root = Math.sqrt(n); + for (let prev of found) { + if (prev > root) { + break; + } else if (n % prev == 0) { + skip = true; + break; + } + } + if (!skip) { + found.push(n); + yield n; + } + } +} + +function measurePrimes() { + let iter = primes(), t0 = Date.now(); + for (let i = 0; i < 10000; i++) { + iter.next(); + } + console.log(`Took ${Date.now() - t0}ms`); +} + +measurePrimes(); diff --git a/html/code/solutions/22_2_timing.js b/html/code/solutions/22_2_timing.js new file mode 100644 index 000000000..d37c83806 --- /dev/null +++ b/html/code/solutions/22_2_timing.js @@ -0,0 +1,20 @@ +function findPath(a, b) { + let work = [[a]]; + for (let path of work) { + let end = path[path.length - 1]; + if (end == b) return path; + for (let next of end.edges) { + if (!work.some(path => path[path.length - 1] == next)) { + work.push(path.concat([next])); + } + } + } +} + +function time(findPath) { + let graph = treeGraph(6, 6); + let startTime = Date.now(); + let result = findPath(graph[0], graph[graph.length - 1]); + console.log(`Path with length ${result.length} found in ${Date.now() - startTime}ms`); +} +time(findPath); diff --git a/html/code/solutions/22_3_optimizing.js b/html/code/solutions/22_3_optimizing.js new file mode 100644 index 000000000..dff729050 --- /dev/null +++ b/html/code/solutions/22_3_optimizing.js @@ -0,0 +1,45 @@ +function time(findPath) { + let graph = treeGraph(6, 6); + let startTime = Date.now(); + let result = findPath(graph[0], graph[graph.length - 1]); + console.log(`Path with length ${result.length} found in ${Date.now() - startTime}ms`); +} + +function findPath_set(a, b) { + let work = [[a]]; + let reached = new Set([a]); + for (let path of work) { + let end = path[path.length - 1]; + if (end == b) return path; + for (let next of end.edges) { + if (!reached.has(next)) { + reached.add(next); + work.push(path.concat([next])); + } + } + } +} + +time(findPath_set); + +function pathToArray(path) { + let result = []; + for (; path; path = path.via) result.unshift(path.at); + return result; +} + +function findPath_list(a, b) { + let work = [{at: a, via: null}]; + let reached = new Set([a]); + for (let path of work) { + if (path.at == b) return pathToArray(path); + for (let next of path.at.edges) { + if (!reached.has(next)) { + reached.add(next); + work.push({at: next, via: path}); + } + } + } +} + +time(findPath_list); diff --git a/html/code/squareworker.js b/html/code/squareworker.js new file mode 100644 index 000000000..58a5bed9e --- /dev/null +++ b/html/code/squareworker.js @@ -0,0 +1,3 @@ +addEventListener("message", event => { + postMessage(event.data * event.data); +}); diff --git a/html/favicon.ico b/html/favicon.ico index 382b70692..4268782d2 100644 Binary files a/html/favicon.ico and b/html/favicon.ico differ diff --git a/html/img b/html/img deleted file mode 120000 index 8e83967d4..000000000 --- a/html/img +++ /dev/null @@ -1 +0,0 @@ -../img/ \ No newline at end of file diff --git a/html/img/Hieres-sur-Amby.png b/html/img/Hieres-sur-Amby.png new file mode 100644 index 000000000..57721e9da Binary files /dev/null and b/html/img/Hieres-sur-Amby.png differ diff --git a/html/img/blockquote.png b/html/img/blockquote.png new file mode 100644 index 000000000..a08559251 Binary files /dev/null and b/html/img/blockquote.png differ diff --git a/html/img/boxed-in.png b/html/img/boxed-in.png new file mode 100644 index 000000000..45bbb40df Binary files /dev/null and b/html/img/boxed-in.png differ diff --git a/html/img/button_disabled.png b/html/img/button_disabled.png new file mode 100644 index 000000000..2e44b2199 Binary files /dev/null and b/html/img/button_disabled.png differ diff --git a/html/img/canvas_beziercurve.png b/html/img/canvas_beziercurve.png new file mode 100644 index 000000000..120e0cafa Binary files /dev/null and b/html/img/canvas_beziercurve.png differ diff --git a/html/img/canvas_circle.png b/html/img/canvas_circle.png new file mode 100644 index 000000000..ab2d266b3 Binary files /dev/null and b/html/img/canvas_circle.png differ diff --git a/html/img/canvas_fill.png b/html/img/canvas_fill.png new file mode 100644 index 000000000..09965aecc Binary files /dev/null and b/html/img/canvas_fill.png differ diff --git a/html/img/canvas_game.png b/html/img/canvas_game.png new file mode 100644 index 000000000..611ac8d36 Binary files /dev/null and b/html/img/canvas_game.png differ diff --git a/html/img/canvas_path.png b/html/img/canvas_path.png new file mode 100644 index 000000000..14231f2c7 Binary files /dev/null and b/html/img/canvas_path.png differ diff --git a/html/img/canvas_pie_chart.png b/html/img/canvas_pie_chart.png new file mode 100644 index 000000000..022289cc2 Binary files /dev/null and b/html/img/canvas_pie_chart.png differ diff --git a/html/img/canvas_quadraticcurve.png b/html/img/canvas_quadraticcurve.png new file mode 100644 index 000000000..121cd4a64 Binary files /dev/null and b/html/img/canvas_quadraticcurve.png differ diff --git a/html/img/canvas_scale.png b/html/img/canvas_scale.png new file mode 100644 index 000000000..373b9c361 Binary files /dev/null and b/html/img/canvas_scale.png differ diff --git a/html/img/canvas_stroke.png b/html/img/canvas_stroke.png new file mode 100644 index 000000000..2a4b38247 Binary files /dev/null and b/html/img/canvas_stroke.png differ diff --git a/html/img/canvas_tree.png b/html/img/canvas_tree.png new file mode 100644 index 000000000..f0b301d40 Binary files /dev/null and b/html/img/canvas_tree.png differ diff --git a/html/img/canvas_triangle.png b/html/img/canvas_triangle.png new file mode 100644 index 000000000..50bcd14cd Binary files /dev/null and b/html/img/canvas_triangle.png differ diff --git a/html/img/cat-animation.png b/html/img/cat-animation.png new file mode 100644 index 000000000..4241c9676 Binary files /dev/null and b/html/img/cat-animation.png differ diff --git a/html/img/cat.png b/html/img/cat.png new file mode 100644 index 000000000..e365386fb Binary files /dev/null and b/html/img/cat.png differ diff --git a/html/img/chapter_picture_00.jpg b/html/img/chapter_picture_00.jpg new file mode 100644 index 000000000..a3f4730cf Binary files /dev/null and b/html/img/chapter_picture_00.jpg differ diff --git a/html/img/chapter_picture_1.jpg b/html/img/chapter_picture_1.jpg new file mode 100644 index 000000000..ebc0689f0 Binary files /dev/null and b/html/img/chapter_picture_1.jpg differ diff --git a/html/img/chapter_picture_10.jpg b/html/img/chapter_picture_10.jpg new file mode 100644 index 000000000..40706b183 Binary files /dev/null and b/html/img/chapter_picture_10.jpg differ diff --git a/html/img/chapter_picture_11.jpg b/html/img/chapter_picture_11.jpg new file mode 100644 index 000000000..8b63ef70b Binary files /dev/null and b/html/img/chapter_picture_11.jpg differ diff --git a/html/img/chapter_picture_12.jpg b/html/img/chapter_picture_12.jpg new file mode 100644 index 000000000..b90572f1c Binary files /dev/null and b/html/img/chapter_picture_12.jpg differ diff --git a/html/img/chapter_picture_13.jpg b/html/img/chapter_picture_13.jpg new file mode 100644 index 000000000..bc3513a2b Binary files /dev/null and b/html/img/chapter_picture_13.jpg differ diff --git a/html/img/chapter_picture_14.jpg b/html/img/chapter_picture_14.jpg new file mode 100644 index 000000000..90af0d741 Binary files /dev/null and b/html/img/chapter_picture_14.jpg differ diff --git a/html/img/chapter_picture_15.jpg b/html/img/chapter_picture_15.jpg new file mode 100644 index 000000000..85462c0be Binary files /dev/null and b/html/img/chapter_picture_15.jpg differ diff --git a/html/img/chapter_picture_16.jpg b/html/img/chapter_picture_16.jpg new file mode 100644 index 000000000..ed2b4e6dd Binary files /dev/null and b/html/img/chapter_picture_16.jpg differ diff --git a/html/img/chapter_picture_17.jpg b/html/img/chapter_picture_17.jpg new file mode 100644 index 000000000..7445f048d Binary files /dev/null and b/html/img/chapter_picture_17.jpg differ diff --git a/html/img/chapter_picture_18.jpg b/html/img/chapter_picture_18.jpg new file mode 100644 index 000000000..7c9a93259 Binary files /dev/null and b/html/img/chapter_picture_18.jpg differ diff --git a/html/img/chapter_picture_19.jpg b/html/img/chapter_picture_19.jpg new file mode 100644 index 000000000..742981d29 Binary files /dev/null and b/html/img/chapter_picture_19.jpg differ diff --git a/html/img/chapter_picture_2.jpg b/html/img/chapter_picture_2.jpg new file mode 100644 index 000000000..3563ae5c0 Binary files /dev/null and b/html/img/chapter_picture_2.jpg differ diff --git a/html/img/chapter_picture_20.jpg b/html/img/chapter_picture_20.jpg new file mode 100644 index 000000000..6a487a9e7 Binary files /dev/null and b/html/img/chapter_picture_20.jpg differ diff --git a/html/img/chapter_picture_21.jpg b/html/img/chapter_picture_21.jpg new file mode 100644 index 000000000..834e9c065 Binary files /dev/null and b/html/img/chapter_picture_21.jpg differ diff --git a/html/img/chapter_picture_3.jpg b/html/img/chapter_picture_3.jpg new file mode 100644 index 000000000..53923d98e Binary files /dev/null and b/html/img/chapter_picture_3.jpg differ diff --git a/html/img/chapter_picture_4.jpg b/html/img/chapter_picture_4.jpg new file mode 100644 index 000000000..9deda2e6f Binary files /dev/null and b/html/img/chapter_picture_4.jpg differ diff --git a/html/img/chapter_picture_5.jpg b/html/img/chapter_picture_5.jpg new file mode 100644 index 000000000..76658b8bf Binary files /dev/null and b/html/img/chapter_picture_5.jpg differ diff --git a/html/img/chapter_picture_6.jpg b/html/img/chapter_picture_6.jpg new file mode 100644 index 000000000..8af17dbc5 Binary files /dev/null and b/html/img/chapter_picture_6.jpg differ diff --git a/html/img/chapter_picture_7.jpg b/html/img/chapter_picture_7.jpg new file mode 100644 index 000000000..5154d33aa Binary files /dev/null and b/html/img/chapter_picture_7.jpg differ diff --git a/html/img/chapter_picture_8.jpg b/html/img/chapter_picture_8.jpg new file mode 100644 index 000000000..e61d44b40 Binary files /dev/null and b/html/img/chapter_picture_8.jpg differ diff --git a/html/img/chapter_picture_9.jpg b/html/img/chapter_picture_9.jpg new file mode 100644 index 000000000..efb340015 Binary files /dev/null and b/html/img/chapter_picture_9.jpg differ diff --git a/html/img/color-field.png b/html/img/color-field.png new file mode 100644 index 000000000..2ee57c267 Binary files /dev/null and b/html/img/color-field.png differ diff --git a/html/img/colored-links.png b/html/img/colored-links.png new file mode 100644 index 000000000..ab4efaf0c Binary files /dev/null and b/html/img/colored-links.png differ diff --git a/html/img/control-io.svg b/html/img/control-io.svg new file mode 100644 index 000000000..1d9ae071e --- /dev/null +++ b/html/img/control-io.svg @@ -0,0 +1,10 @@ + + +synchronous, single thread of controlsynchronous, two threads of controlasynchronous diff --git a/html/img/controlflow-if.svg b/html/img/controlflow-if.svg new file mode 100644 index 000000000..a0f793a4a --- /dev/null +++ b/html/img/controlflow-if.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/html/img/controlflow-loop.svg b/html/img/controlflow-loop.svg new file mode 100644 index 000000000..290b771a0 --- /dev/null +++ b/html/img/controlflow-loop.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/html/img/controlflow-nested-if.svg b/html/img/controlflow-nested-if.svg new file mode 100644 index 000000000..9ab41a8aa --- /dev/null +++ b/html/img/controlflow-nested-if.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/html/img/controlflow-straight.svg b/html/img/controlflow-straight.svg new file mode 100644 index 000000000..f94d671e7 --- /dev/null +++ b/html/img/controlflow-straight.svg @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/html/img/cos_sin.svg b/html/img/cos_sin.svg new file mode 100644 index 000000000..67608feaf --- /dev/null +++ b/html/img/cos_sin.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + cos(¼π) + sin(¼π) + + + + + + cos(-⅔π) + sin(-⅔π) + sin(-⅔π) + diff --git a/html/img/cover.jpg b/html/img/cover.jpg new file mode 100644 index 000000000..aca3419b4 Binary files /dev/null and b/html/img/cover.jpg differ diff --git a/html/img/darkblue.png b/html/img/darkblue.png new file mode 100644 index 000000000..3af8c8a80 Binary files /dev/null and b/html/img/darkblue.png differ diff --git a/html/img/display.png b/html/img/display.png new file mode 100644 index 000000000..0e56b8851 Binary files /dev/null and b/html/img/display.png differ diff --git a/html/img/drag-bar.png b/html/img/drag-bar.png new file mode 100644 index 000000000..aebf6f56b Binary files /dev/null and b/html/img/drag-bar.png differ diff --git a/html/img/exercise_shapes.png b/html/img/exercise_shapes.png new file mode 100644 index 000000000..702899332 Binary files /dev/null and b/html/img/exercise_shapes.png differ diff --git a/html/img/flood-grid.svg b/html/img/flood-grid.svg new file mode 100644 index 000000000..4cb0b075a --- /dev/null +++ b/html/img/flood-grid.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/html/img/form_fields.png b/html/img/form_fields.png new file mode 100644 index 000000000..667b3b6fa Binary files /dev/null and b/html/img/form_fields.png differ diff --git a/html/img/form_select.png b/html/img/form_select.png new file mode 100644 index 000000000..1e5f254c3 Binary files /dev/null and b/html/img/form_select.png differ diff --git a/html/img/game-grid.svg b/html/img/game-grid.svg new file mode 100644 index 000000000..954b028f7 --- /dev/null +++ b/html/img/game-grid.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/html/img/game_simpleLevel.png b/html/img/game_simpleLevel.png new file mode 100644 index 000000000..3f00e008f Binary files /dev/null and b/html/img/game_simpleLevel.png differ diff --git a/html/img/generated/.keep b/html/img/generated/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/html/img/hat.png b/html/img/hat.png new file mode 100644 index 000000000..f2889e84c Binary files /dev/null and b/html/img/hat.png differ diff --git a/html/img/help-field.png b/html/img/help-field.png new file mode 100644 index 000000000..2324da542 Binary files /dev/null and b/html/img/help-field.png differ diff --git a/html/img/highlighted.png b/html/img/highlighted.png new file mode 100644 index 000000000..be41fd6dc Binary files /dev/null and b/html/img/highlighted.png differ diff --git a/html/img/holberton.png b/html/img/holberton.png new file mode 100644 index 000000000..918b84fdf Binary files /dev/null and b/html/img/holberton.png differ diff --git a/html/img/home-page.png b/html/img/home-page.png new file mode 100644 index 000000000..42418121a Binary files /dev/null and b/html/img/home-page.png differ diff --git a/html/img/html-boxes.svg b/html/img/html-boxes.svg new file mode 100644 index 000000000..f985457db --- /dev/null +++ b/html/img/html-boxes.svg @@ -0,0 +1,23 @@ + + +herea.I also wrote a book! Read itpHello, I am Marijn and this is...pMy home pageh1bodyMy home pagetitleheadhtml diff --git a/html/img/html-links.svg b/html/img/html-links.svg new file mode 100644 index 000000000..d6a3b1e11 --- /dev/null +++ b/html/img/html-links.svg @@ -0,0 +1,64 @@ + + + I also wrote a book! ... + p + + Hello, I am Marijn... + p + + My home page + h1 + + body + + 0 + 1 + 2 + + + + + + + + + + childNodes + + + firstChild + + + lastChild + + + previousSibling + + + nextSibling + + + parentNode + diff --git a/html/img/html-tree.svg b/html/img/html-tree.svg new file mode 100644 index 000000000..cbf501640 --- /dev/null +++ b/html/img/html-tree.svg @@ -0,0 +1,26 @@ + + +htmlheadtitleMy home pagebodyh1My home pagepHello! I am...pI also wrote...herea. diff --git a/html/img/line-grid.svg b/html/img/line-grid.svg new file mode 100644 index 000000000..95bbf53eb --- /dev/null +++ b/html/img/line-grid.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/html/img/linked-list.svg b/html/img/linked-list.svg new file mode 100644 index 000000000..9e00738ea --- /dev/null +++ b/html/img/linked-list.svg @@ -0,0 +1,13 @@ + + +value: 1rest:value: 2rest:value: 3rest: null diff --git a/html/img/middle_east_graph.png b/html/img/middle_east_graph.png new file mode 100644 index 000000000..430e88b14 Binary files /dev/null and b/html/img/middle_east_graph.png differ diff --git a/html/img/middle_east_graph_random.png b/html/img/middle_east_graph_random.png new file mode 100644 index 000000000..c7098f852 Binary files /dev/null and b/html/img/middle_east_graph_random.png differ diff --git a/html/img/mirror.svg b/html/img/mirror.svg new file mode 100644 index 000000000..6f3c6c169 --- /dev/null +++ b/html/img/mirror.svg @@ -0,0 +1,11 @@ + + +mirror1234 diff --git a/html/img/nextjournal.png b/html/img/nextjournal.png new file mode 100644 index 000000000..e1bc6ea52 Binary files /dev/null and b/html/img/nextjournal.png differ diff --git a/html/img/object.jpg b/html/img/object.jpg new file mode 100644 index 000000000..d9c24dd60 Binary files /dev/null and b/html/img/object.jpg differ diff --git a/html/img/object_full.jpg b/html/img/object_full.jpg new file mode 100644 index 000000000..3b3c4f12b Binary files /dev/null and b/html/img/object_full.jpg differ diff --git a/html/img/ostrich.png b/html/img/ostrich.png new file mode 100644 index 000000000..1dbe890a4 Binary files /dev/null and b/html/img/ostrich.png differ diff --git a/html/img/parcel2x.png b/html/img/parcel2x.png new file mode 100644 index 000000000..ff518258a Binary files /dev/null and b/html/img/parcel2x.png differ diff --git a/html/img/pixel_editor.png b/html/img/pixel_editor.png new file mode 100644 index 000000000..e1c80901f Binary files /dev/null and b/html/img/pixel_editor.png differ diff --git a/html/img/pizza-squirrel.svg b/html/img/pizza-squirrel.svg new file mode 100644 index 000000000..4cc93317c --- /dev/null +++ b/html/img/pizza-squirrel.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + +No squirrel, no pizza76Squirrel, no pizza4No squirrel, pizza9Squirrel, pizza1 \ No newline at end of file diff --git a/html/img/player.png b/html/img/player.png new file mode 100644 index 000000000..1244769ac Binary files /dev/null and b/html/img/player.png differ diff --git a/html/img/player_big.png b/html/img/player_big.png new file mode 100644 index 000000000..de305fcc7 Binary files /dev/null and b/html/img/player_big.png differ diff --git a/html/img/prompt.png b/html/img/prompt.png new file mode 100644 index 000000000..d485a0ef9 Binary files /dev/null and b/html/img/prompt.png differ diff --git a/html/img/rabbits.svg b/html/img/rabbits.svg new file mode 100644 index 000000000..5ee2dac16 --- /dev/null +++ b/html/img/rabbits.svg @@ -0,0 +1,13 @@ + + +toString: <function>...teeth: "small"speak: <function>killerRabbitteeth: "long, sharp, ..."type: "killer"RabbitprototypeObjectcreate: <function>prototype... diff --git a/html/img/re_number.svg b/html/img/re_number.svg new file mode 100644 index 000000000..519a38ff3 --- /dev/null +++ b/html/img/re_number.svg @@ -0,0 +1,15 @@ +Start of linegroup #1One of:01bOne of:digit-afhdigitEnd of line diff --git a/html/img/re_pigchickens.svg b/html/img/re_pigchickens.svg new file mode 100644 index 000000000..fac5b4b86 --- /dev/null +++ b/html/img/re_pigchickens.svg @@ -0,0 +1,17 @@ +digit group #1pigcowchickens diff --git a/html/img/re_slow.svg b/html/img/re_slow.svg new file mode 100644 index 000000000..493c9bc99 --- /dev/null +++ b/html/img/re_slow.svg @@ -0,0 +1,175 @@ + + + + + + + + + + + + Created with Raphaël 2.1.0 + + + + "b" + + + + Group #1 + + + + One of: + + + + "1" + + + + "0" + + + + + + diff --git a/html/img/robot_idle.png b/html/img/robot_idle.png new file mode 100644 index 000000000..605293ef1 Binary files /dev/null and b/html/img/robot_idle.png differ diff --git a/html/img/robot_idle2x.png b/html/img/robot_idle2x.png new file mode 100644 index 000000000..d1b36b960 Binary files /dev/null and b/html/img/robot_idle2x.png differ diff --git a/html/img/robot_moving.gif b/html/img/robot_moving.gif new file mode 100644 index 000000000..d73feb3b3 Binary files /dev/null and b/html/img/robot_moving.gif differ diff --git a/html/img/robot_moving2x.gif b/html/img/robot_moving2x.gif new file mode 100644 index 000000000..3e145bbae Binary files /dev/null and b/html/img/robot_moving2x.gif differ diff --git a/html/img/skillsharing.png b/html/img/skillsharing.png new file mode 100644 index 000000000..48781637a Binary files /dev/null and b/html/img/skillsharing.png differ diff --git a/html/img/sprites.png b/html/img/sprites.png new file mode 100644 index 000000000..2a77ab991 Binary files /dev/null and b/html/img/sprites.png differ diff --git a/html/img/sprites_big.png b/html/img/sprites_big.png new file mode 100644 index 000000000..afa6e2c85 Binary files /dev/null and b/html/img/sprites_big.png differ diff --git a/html/img/svg-demo.png b/html/img/svg-demo.png new file mode 100644 index 000000000..1efcff823 Binary files /dev/null and b/html/img/svg-demo.png differ diff --git a/html/img/syntax_tree.svg b/html/img/syntax_tree.svg new file mode 100644 index 000000000..3aab52091 --- /dev/null +++ b/html/img/syntax_tree.svg @@ -0,0 +1,11 @@ + + +dodefinex10if>x5print"large"print"small" \ No newline at end of file diff --git a/html/img/tamil.png b/html/img/tamil.png new file mode 100644 index 000000000..dc7591f74 Binary files /dev/null and b/html/img/tamil.png differ diff --git a/html/img/transform.svg b/html/img/transform.svg new file mode 100644 index 000000000..553cd6e9a --- /dev/null +++ b/html/img/transform.svg @@ -0,0 +1,11 @@ + + +translate(50, 50)rotate(0.1*Math.PI)rotate(0.1*Math.PI)translate(50, 50) diff --git a/html/img/tree_graph.png b/html/img/tree_graph.png new file mode 100644 index 000000000..9f19e8e5a Binary files /dev/null and b/html/img/tree_graph.png differ diff --git a/html/img/unicycle.svg b/html/img/unicycle.svg new file mode 100644 index 000000000..fb261d0dd --- /dev/null +++ b/html/img/unicycle.svg @@ -0,0 +1,277 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/html/img/village.png b/html/img/village.png new file mode 100644 index 000000000..886c8fce1 Binary files /dev/null and b/html/img/village.png differ diff --git a/html/img/village2x.png b/html/img/village2x.png new file mode 100644 index 000000000..8b1d7c765 Binary files /dev/null and b/html/img/village2x.png differ diff --git a/html/img/weresquirrel.png b/html/img/weresquirrel.png new file mode 100644 index 000000000..c24d684c5 Binary files /dev/null and b/html/img/weresquirrel.png differ diff --git a/html/img/weresquirrel.svg b/html/img/weresquirrel.svg new file mode 100644 index 000000000..687c3675a --- /dev/null +++ b/html/img/weresquirrel.svg @@ -0,0 +1,6994 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/html/index.html b/html/index.html index 970c32d8c..cad417d44 100644 --- a/html/index.html +++ b/html/index.html @@ -2,7 +2,7 @@ - Eloquent JavaScript + Mahir JavaScript + +

      - - Cover image - + Cover image

      -

      Eloquent JavaScript
      4th edition (2024)

      +

      Mahir JavaScript
      Edisi pertama (2025)

      -

      This is a book about JavaScript, programming, and the wonders of - the digital. You can read it online here, or buy your own - paperback copy.

      +

      Ini adalah buku mengenai JavaScript, pemrograman, dan keajaiban digital. Anda bisa membacanya secara daring di sini.

      -

      Written by Marijn Haverbeke.

      +

      Ditulis oleh Marijn Haverbeke. Diterjemahkan oleh Hanief Utama.

      -

      Licensed under - a Creative - Commons attribution-noncommercial license. All code in this book - may also be considered licensed under - an MIT license.

      - -

      Illustrations by various artists: Cover - by Péchane Sumi-e. Chapter - illustrations by Madalina - Tantareanu. Pixel art in Chapters 7 and 16 by Antonio Perdomo - Pastor. Regular expression diagrams in Chapter 9 generated - with regexper.com by Jeff - Avallone. Game concept for Chapter 16 - by Thomas Palef.

      +

      Berlisensi + Creative + Commons attribution-noncommercial license. Seluruh kode dalam buku berlisensi MIT license.

      + +

      Ilustrasi oleh beberapa seniman: sampul oleh Hanief Utama. Ilustrasi bab oleh Madalina + Tantareanu. Ilustrasi piksel di bab 7 dan 16 oleh Antonio Perdomo + Pastor. Diagram ekspresi reguler di bab 9 dibuat menggunakan regexper.com oleh Jeff Avallone. Konsep permainan di bab 16 oleh + Thomas Palef.

      diff --git a/img/cover.jpg b/img/cover.jpg index 36ac9e850..aca3419b4 100644 Binary files a/img/cover.jpg and b/img/cover.jpg differ diff --git a/00_intro.md b/intro.md similarity index 99% rename from 00_intro.md rename to intro.md index 1d14eef57..1a9c2e7e9 100644 --- a/00_intro.md +++ b/intro.md @@ -274,4 +274,4 @@ console.log(factorial(8)); // → 40320 ``` -Good luck! +Good luck! \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..2dcbfd85d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1633 @@ +{ + "name": "Eloquent-JavaScript", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "Eloquent-JavaScript", + "version": "0.1.0", + "license": "CC BY-NC 3.0", + "dependencies": { + "@codemirror/lang-css": "^6.2.0", + "@codemirror/lang-html": "^6.4.0", + "@codemirror/lang-javascript": "^6.2.0", + "@codemirror/language": "^6.9.0", + "@codemirror/legacy-modes": "^6.3.0", + "@codemirror/state": "^6.2.0", + "@codemirror/view": "^6.20.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.1.0", + "@rollup/plugin-node-resolve": "^15.0.0", + "@rollup/plugin-terser": "^0.4.4", + "acorn": "^8.0.0", + "acorn-walk": "^8.0.0", + "codemirror": "^6.0.0", + "jszip": "^3.10.0", + "markdown-it": "^14.0.0", + "markdown-it-sub": "^2.0.0", + "markdown-it-sup": "^2.0.0", + "mime": "^2.3.1", + "mold-template": "^2.0.1", + "rollup": "^3.28.0" + }, + "devDependencies": { + "jsdom": "^20.0.0", + "promise": "^8.0.1" + } + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.18.6", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz", + "integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz", + "integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-css": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", + "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/css": "^1.1.7" + } + }, + "node_modules/@codemirror/lang-html": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz", + "integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.0" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz", + "integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.1.tgz", + "integrity": "sha512-5kS1U7emOGV84vxC+ruBty5sUgcD0te6dyupyRVG2zaSjhTDM73LhVKUtVwiqSe6QwmEoA4SCiU8AKPFyumAWQ==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/legacy-modes": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.1.tgz", + "integrity": "sha512-DJYQQ00N1/KdESpZV7jg9hafof/iBNp9h7TYo1SLMk86TWl9uDsVdho2dzd81K+v4retmK6mdC7WpuOQDytQqw==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.8.5", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz", + "integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz", + "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", + "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.37.2", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.37.2.tgz", + "integrity": "sha512-XD3LdgQpxQs5jhOOZ2HRVT+Rj59O4Suc7g2ULvZ+Yi8eCkickrkZ5JFuoDhs2ST1mNI5zSsNYgR3NGa4OUrbnw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lezer/common": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", + "license": "MIT" + }, + "node_modules/@lezer/css": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.2.1.tgz", + "integrity": "sha512-2F5tOqzKEKbCUNraIXc0f6HKeyKlmMWJnBB0i4XW6dJgssrZO/YlZ2pY5xgyqDleqqhiNJ3dQhbrV2aClZQMvg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/highlight": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/html": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz", + "integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.1.tgz", + "integrity": "sha512-ATOImjeVJuvgm3JQ/bpo2Tmv55HSScE2MTPnKRMRIPx2cLhHGyX2VnqpHhtIV1tVzIjZDbcWQm+NCTF40ggZVw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", + "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "license": "MIT", + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz", + "integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "license": "MIT" + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", + "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "license": "MIT", + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "license": "MIT" + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it-sub": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-sub/-/markdown-it-sub-2.0.0.tgz", + "integrity": "sha512-iCBKgwCkfQBRg2vApy9vx1C1Tu6D8XYo8NvevI3OlwzBRmiMtsJ2sXupBgEA7PPxiDwNni3qIUkhZ6j5wofDUA==", + "license": "MIT" + }, + "node_modules/markdown-it-sup": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-sup/-/markdown-it-sup-2.0.0.tgz", + "integrity": "sha512-5VgmdKlkBd8sgXuoDoxMpiU+BiEt3I49GItBzzw7Mxq9CxvnhE/k09HFli09zgfFDRixDQDfDxi0mgBCXtaTvA==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mold-template": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mold-template/-/mold-template-2.0.2.tgz", + "integrity": "sha512-8VqPPC8l3MtB5rrkekA/CKINh+X18KumW2lCr2xId8cP6bxONUOEhQW+cYM8gfsuS1A8bYo1w122/vyFsuWYaQ==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nwsapi": { + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", + "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", + "dev": true, + "license": "MIT" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/promise": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", + "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "asap": "~2.0.6" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.30.0.tgz", + "integrity": "sha512-kQvGasUgN+AlWGliFn2POSajRQEsULVYFGTvOZmK06d7vCD+YhZztt70kGk3qaeAXeWYL5eO7zx+rAubBc55eA==", + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/smob": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", + "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "license": "MIT" + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/terser": { + "version": "5.43.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", + "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.14.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/pdf/book.tex b/pdf/book.tex index 517b55cd1..b59a9b2bb 100644 --- a/pdf/book.tex +++ b/pdf/book.tex @@ -10,11 +10,25 @@ \usepackage{fontspec} \usepackage{xcolor} \usepackage{pdfpages} -\usepackage{arabxetex} \usepackage{makeidx} - -% epigraph is used to style chapter quotes -\usepackage{epigraph} +\IfFontExistsTF{Geeza Pro}{ + \newfontfamily{\arabicfont}{Geeza Pro} +}{ + \newfontfamily{\arabicfont}{Times New Roman} +} +\newcommand{\textarab}[1]{{\arabicfont #1}} + +\newlength{\epigraphwidth} +\newlength{\epigraphrule} +\newcommand{\epigraph}[2]{% + \begin{flushright} + \begin{minipage}{.8\textwidth} + \itshape #1\par + \raggedleft #2 + \end{minipage} + \end{flushright} +} +\newcommand{\epigraphhead}[2][]{#2} \setlength{\epigraphwidth}{.8\textwidth} \setlength{\epigraphrule}{0pt} @@ -43,13 +57,29 @@ \setcounter{secnumdepth}{0} \setcounter{tocdepth}{1} -\setmonofont[Scale=0.8]{Inconsolata LGC} +\IfFontExistsTF{Inconsolata LGC}{ + \setmonofont[Scale=0.8]{Inconsolata LGC} +}{ + \setmonofont[Scale=0.8]{Inconsolata} +} \defaultfontfeatures[\emojifont]{Scale=1.15} -\newfontface{\emojifont}{Symbola_hint.ttf} -\newfontfamily{\cjkfont}{TW-Sung} +\IfFontExistsTF{Symbola_hint.ttf}{ + \newfontface{\emojifont}{Symbola_hint.ttf} +}{ + \newfontface{\emojifont}{Apple Symbols} +} +\IfFontExistsTF{TW-Sung}{ + \newfontfamily{\cjkfont}{TW-Sung} +}{ + \newfontfamily{\cjkfont}{Songti TC} +} \setTransitionsFor{MiscellaneousSymbolsAndPictographs}{\emojifont}{\normalfont}{} -\newfontfamily{\cinzel}{Cinzel} +\IfFontExistsTF{Cinzel}{ + \newfontfamily{\cinzel}{Cinzel} +}{ + \newfontfamily{\cinzel}{Times New Roman} +} \setkomafont{disposition}{\bfseries\cinzel} \definecolor{silver-chalice}{HTML}{AAAAAA} \setkomafont{chapterprefix}{\small\color{silver-chalice}} @@ -57,12 +87,14 @@ \pagestyle{plain} -\usepackage{newunicodechar} -\newunicodechar{π}{$\pi$} -\newunicodechar{ϕ}{$\varphi$} -\newunicodechar{≈}{$\approx$} -\newunicodechar{β}{\ss} -\newunicodechar{⮪}{\emojifont{⮪}} +\IfFileExists{newunicodechar.sty}{ + \usepackage{newunicodechar} + \newunicodechar{π}{$\pi$} + \newunicodechar{ϕ}{$\varphi$} + \newunicodechar{≈}{$\approx$} + \newunicodechar{β}{\ss} + \newunicodechar{⮪}{\emojifont{⮪}} +}{} \graphicspath{{../}} \definecolor{coveryellow}{rgb}{0.997,0.840,0.122} diff --git a/src/build_book_pdf.mjs b/src/build_book_pdf.mjs new file mode 100644 index 000000000..d3dd20ad7 --- /dev/null +++ b/src/build_book_pdf.mjs @@ -0,0 +1,49 @@ +import {spawnSync} from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; + +let [, , name, output] = process.argv; + +if (!name || !output) { + console.error("Usage: node src/build_book_pdf.mjs "); + process.exit(1); +} + +function texBinDirs() { + let dirs = ["/Library/TeX/texbin", "/usr/texbin"]; + let root = "/usr/local/texlive"; + if (fs.existsSync(root)) { + for (let entry of fs.readdirSync(root)) { + let binDir = path.join(root, entry, "bin", "universal-darwin"); + if (fs.existsSync(binDir)) dirs.push(binDir); + } + } + return dirs; +} + +let env = { + ...process.env, + PATH: [...texBinDirs(), process.env.PATH || ""].join(":") +}; + +function run(command, args) { + let result = spawnSync(command, args, {cwd: "pdf", stdio: "inherit", env}); + if (result.error) { + if (result.error.code == "ENOENT" || result.error.code == "EACCES") { + console.error(`Required command is unavailable: ${command}`); + process.exit(1); + } + throw result.error; + } + if (result.status) process.exit(result.status); +} + +for (let i = 0; i < 2; i++) run("xelatex", [`${name}.tex`]); +for (let i = 0; i < 2; i++) run("makeindex", ["-o", `${name}.ind`, `${name}.idx`]); +run("xelatex", [`${name}.tex`]); + +let logFile = path.join("pdf", `${name}.log`); +while (/^LaTeX Warning: Label\(s\) may have changed/m.test(fs.readFileSync(logFile, "utf8"))) + run("xelatex", [`${name}.tex`]); + +fs.copyFileSync(path.join("pdf", `${name}.pdf`), output); diff --git a/src/build_epub.mjs b/src/build_epub.mjs new file mode 100644 index 000000000..96b7c1585 --- /dev/null +++ b/src/build_epub.mjs @@ -0,0 +1,80 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import JSZip from "jszip"; + +let [, , output] = process.argv; + +if (!output) { + console.error("Usage: node src/build_epub.mjs "); + process.exit(1); +} + +let epubDir = "epub"; + +async function walk(dir) { + let entries = await fs.readdir(dir, {withFileTypes: true}); + let files = []; + for (let entry of entries) { + if (entry.name.startsWith(".")) continue; + let full = path.join(dir, entry.name); + if (entry.isDirectory()) files.push(...await walk(full)); + else files.push(full); + } + return files; +} + +async function ensureDir(file) { + await fs.mkdir(path.dirname(file), {recursive: true}); +} + +function mediaType(file) { + if (file.endsWith(".svg")) return "image/svg+xml"; + if (file.endsWith(".png")) return "image/png"; + if (file.endsWith(".jpg")) return "image/jpeg"; + throw new Error(`Unknown image type: ${file}`); +} + +async function copyReferencedImages() { + let xhtmlFiles = (await walk(epubDir)).filter(file => file.endsWith(".xhtml")); + let seen = new Set(); + let imageRefs = []; + for (let file of xhtmlFiles) { + let content = await fs.readFile(file, "utf8"); + for (let match of content.matchAll(/]*\bsrc="([^"]+)"/g)) { + let href = match[1]; + let source = path.normalize(path.resolve(href)); + let target = path.normalize(path.resolve(epubDir, match[1])); + if (seen.has(target)) continue; + seen.add(target); + if (source != target) { + await ensureDir(target); + await fs.copyFile(source, target); + } + imageRefs.push(path.relative(epubDir, target).split(path.sep).join("/")); + } + } + return imageRefs.sort(); +} + +async function writeContentOpf(images) { + let items = images.map(file => + ` ` + ); + let template = await fs.readFile(path.join(epubDir, "content.opf.src"), "utf8"); + await fs.writeFile(path.join(epubDir, "content.opf"), template.replace("{{images}}", items.join("\n"))); +} + +async function writeArchive() { + let zip = new JSZip(); + zip.file("mimetype", await fs.readFile(path.join(epubDir, "mimetype")), {compression: "STORE"}); + for (let file of (await walk(epubDir)).sort()) { + let relative = path.relative(epubDir, file).split(path.sep).join("/"); + if (relative == "mimetype" || relative.endsWith(".src")) continue; + zip.file(relative, await fs.readFile(file)); + } + await fs.writeFile(output, await zip.generateAsync({type: "nodebuffer", compression: "DEFLATE"})); +} + +let images = await copyReferencedImages(); +await writeContentOpf(images); +await writeArchive(); diff --git a/src/build_mobile_tex.mjs b/src/build_mobile_tex.mjs new file mode 100644 index 000000000..c025236e6 --- /dev/null +++ b/src/build_mobile_tex.mjs @@ -0,0 +1,17 @@ +import fs from "node:fs"; + +let [, , input, output] = process.argv; + +if (!input || !output) { + console.error("Usage: node src/build_mobile_tex.mjs "); + process.exit(1); +} + +let tex = fs.readFileSync(input, "utf8") + .replace( + "natbib}", + "natbib}\n\\usepackage[a5paper, left=5mm, right=5mm]{geometry}" + ) + .replace("setmonofont.Scale=0.8.", "setmonofont[Scale=0.75]"); + +fs.writeFileSync(output, tex); diff --git a/src/make_zip.mjs b/src/make_zip.mjs new file mode 100644 index 000000000..29b379aa6 --- /dev/null +++ b/src/make_zip.mjs @@ -0,0 +1,21 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import JSZip from "jszip"; + +let [, , output, ...files] = process.argv; + +if (!output || !files.length) { + console.error("Usage: node src/make_zip.mjs [file...]"); + process.exit(1); +} + +let zip = new JSZip(); + +for (let file of files) { + let stat = await fs.stat(file); + if (!stat.isFile()) continue; + zip.file(file.split(path.sep).join("/"), await fs.readFile(file)); +} + +let archive = await zip.generateAsync({type: "nodebuffer"}); +await fs.writeFile(output, archive);