diff --git a/DESIGN.md b/DESIGN.md
index 5ed3edb..8333d96 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -1,4 +1,4 @@
-# Diretrizes de tema — claro e escuro
+# Diretrizes de tema: claro e escuro
Regras para manter os dois temas consistentes. Não tratam da identidade visual
(degradê da marca, arredondamentos, hierarquia): essas decisões já estão tomadas
@@ -10,7 +10,7 @@ e não mudam.
A escala do Tailwind anda de 100 em 100: `50, 100, 200, 300 … 900, 950`.
Os meios-tons (`150, 250, 350, 450, 550, 650, 750, 850`) **não existem por
-padrão** — uma classe como `text-slate-350` não gera CSS nenhum, e o elemento
+padrão**. Uma classe como `text-slate-350` não gera CSS nenhum, e o elemento
herda silenciosamente a cor do pai. Era a causa da maior parte das
inconsistências: o mesmo texto ficava certo num tema e lavado no outro.
@@ -38,9 +38,9 @@ classe com um valor que não está na escala.
No tema escuro o fundo é escuro, então o texto precisa **clarear**.
- **Texto:** o número do `dark:` é sempre **menor** que o do claro.
- `text-slate-800 dark:text-slate-200` ✅ — `text-slate-400 dark:text-slate-500` ❌
+ `text-slate-800 dark:text-slate-200` ✅ · `text-slate-400 dark:text-slate-500` ❌
- **Fundo e borda:** o número do `dark:` é sempre **maior**.
- `bg-white dark:bg-slate-900` ✅ — `border-slate-200 dark:border-slate-800` ✅
+ `bg-white dark:bg-slate-900` ✅ · `border-slate-200 dark:border-slate-800` ✅
Um par invertido não quebra o build e não aparece em revisão de código: só
enfraquece o contraste justamente onde ele deveria aumentar. Havia 71 deles.
@@ -63,7 +63,7 @@ elemento gráfico, **3:1**.
| Menor ênfase | `text-slate-500 dark:text-slate-450` | 4.8 | 5.2 |
`text-slate-500 dark:text-slate-400` é o par mais usado do projeto (100
-ocorrências) — é o rótulo em caixa alta, a legenda, o texto de apoio.
+ocorrências): é o rótulo em caixa alta, a legenda, o texto de apoio.
**Não use** `slate-400` como cor de texto no tema claro: sobre branco dá 2.56:1.
Era o erro mais repetido.
@@ -72,7 +72,7 @@ Era o erro mais repetido.
## 4. Exceção: superfícies escuras nos dois temas
-Alguns blocos são escuros independente do tema — painéis de CTA com degradê,
+Alguns blocos são escuros independente do tema: painéis de CTA com degradê,
bloco de código, tooltip do diagrama de pinos. Dentro deles:
- **Não use variante `dark:`.** O fundo não muda, então o texto também não deve.
@@ -101,11 +101,11 @@ aparência entre os temas. Botão carrega a **própria cor**, igual nos dois tem
| Papel | Classe | Contraste com texto branco |
|-------|--------|---------------------------|
-| Ação principal | `bg-gradient-to-r from-purple-600 to-indigo-600 text-white` | 5.4 – 6.3 |
+| Ação principal | `bg-gradient-to-r from-purple-600 to-indigo-600 text-white` | 5.4 a 6.3 |
| Ação secundária | `bg-blue-600 hover:bg-blue-700 text-white` | 5.2 |
-| Ação terciária | `bg-white dark:bg-slate-900` + borda + texto neutro | — |
+| Ação terciária | `bg-white dark:bg-slate-900` + borda + texto neutro | · |
-Como a cor é a mesma nos dois temas, o texto branco também é — sem variante
+Como a cor é a mesma nos dois temas, o texto branco também é, sem variante
`dark:`. Isso vale inclusive dentro de painel escuro: o botão de gradiente
funciona sobre fundo claro e escuro.
@@ -130,17 +130,17 @@ Com o conteúdo pareado: título `text-slate-900 dark:text-white`, corpo
## 4.3. Separação de superfície no tema claro
-Entre near-whites o contraste é sempre baixo — quem separa o card do fundo é a
+Entre near-whites o contraste é sempre baixo. Quem separa o card do fundo é a
**borda**, não o fundo. Por isso:
| Camada | Valor | Contraste |
|--------|-------|-----------|
-| Fundo da página | `from-slate-100 via-slate-50 to-purple-100/40` | — |
+| Fundo da página | `from-slate-100 via-slate-50 to-purple-100/40` | · |
| Card | `bg-white` (sólido, sem `/70` ou `/80`) | 1.10 vs fundo |
| Borda do card | `border-slate-300` | 1.48 vs card |
| Divisória interna | `border-slate-200` | mais sutil |
-`border-slate-200/60` sobre card branco dava 1.14 — imperceptível. Não use borda
+`border-slate-200/60` sobre card branco dava 1.14, praticamente imperceptível. Não use borda
com opacidade para delimitar card.
---
@@ -155,12 +155,12 @@ Cada série tem sua cor em [`public/series.json`](public/series.json) (campo
```
Esses valores são iguais nos dois temas. Ao usá-los como **fundo**, mantenha o
-alfa baixo (`18`–`30` em hex) para que funcione sobre superfície clara e escura.
-Como **texto**, só sobre superfície neutra — nunca sobre outra cor de série.
+alfa baixo (de `18` a `30` em hex) para que funcione sobre superfície clara e escura.
+Como **texto**, só sobre superfície neutra, nunca sobre outra cor de série.
### Nunca escreva cor fixa em `style` inline
-`style` inline vence qualquer classe — inclusive as variantes `dark:` e
+`style` inline vence qualquer classe, inclusive as variantes `dark:` e
`hover:`, que deixam de existir para aquele elemento. Só cor **vinda do dado**
pode ir no inline.
@@ -178,7 +178,7 @@ style={{ borderColor: isSelected ? serie.cor : undefined }}
Esse bug é traiçoeiro porque **some depois do primeiro hover**: o
`onMouseLeave` faz `style.borderColor = ''`, que apaga o inline e devolve o
-controle à classe. A borda passa a aparecer e nunca mais some — dando a
+controle à classe. A borda passa a aparecer e nunca mais some, dando a
impressão de que só falha "antes de passar o mouse".
---
diff --git a/app/comparacao/page.jsx b/app/comparacao/page.jsx
index 0b82142..074d21f 100644
--- a/app/comparacao/page.jsx
+++ b/app/comparacao/page.jsx
@@ -154,7 +154,7 @@ export default function Comparacao() {
const renderValue = (value) => {
if (!value || value === "Não") {
- return —;
+ return Não;
}
if (value === "Sim") {
return (
diff --git a/app/globals.css b/app/globals.css
index 4a03a64..83bfb51 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -13,8 +13,8 @@
Meios-tons de slate.
A escala do Tailwind anda de 100 em 100 (50, 100, 200 ... 950), então
- classes como text-slate-350 ou border-slate-850 — usadas em ~190 pontos
- do projeto — não geravam CSS nenhum: o elemento herdava a cor do pai e
+ classes como text-slate-350 ou border-slate-850, usadas em cerca de 190
+ pontos do projeto, não geravam CSS nenhum: o elemento herdava a cor do pai e
ficava diferente em cada tema, sem padrão.
Cada valor abaixo é o ponto médio exato entre os dois degraus oficiais
diff --git a/app/glossario/page.jsx b/app/glossario/page.jsx
index 34f694f..98aa32d 100644
--- a/app/glossario/page.jsx
+++ b/app/glossario/page.jsx
@@ -41,7 +41,7 @@ export default function Glossario() {
},
{
termo: "Aceleradores Criptográficos",
- definicao: "Blocos de hardware dedicados que executam algoritmos de criptografia muito mais rápido e com menor consumo do que a CPU. Praticamente toda série tem algum, mas o conjunto varia bastante — por isso vale olhar item por item.",
+ definicao: "Blocos de hardware dedicados que executam algoritmos de criptografia muito mais rápido e com menor consumo do que a CPU. Praticamente toda série tem algum, mas o conjunto varia bastante, por isso vale olhar item por item.",
exemplo: "ESP32-C2 e ESP32-C61 não têm acelerador AES: só SHA e curvas elípticas.",
},
{
@@ -51,7 +51,7 @@ export default function Glossario() {
},
{
termo: "ECDSA",
- definicao: "Algoritmo de assinatura digital baseado em curvas elípticas. Ter o periférico ECDSA no silício significa assinar e verificar sem carregar a CPU — é o que o Matter exige para o atestado de dispositivo.",
+ definicao: "Algoritmo de assinatura digital baseado em curvas elípticas. Ter o periférico ECDSA no silício significa assinar e verificar sem carregar a CPU. É o que o Matter exige para o atestado de dispositivo.",
exemplo: "Só ESP32-S31, C5, C61, P4, H2 e H4 têm ECDSA em hardware.",
},
{
diff --git a/app/seletor/page.jsx b/app/seletor/page.jsx
index b69dcdf..26a9ffd 100644
--- a/app/seletor/page.jsx
+++ b/app/seletor/page.jsx
@@ -386,7 +386,7 @@ export default function Seletor() {
if (answers.security === "matter") {
if (temECDSA) {
scores[key] += 30;
- reasons[key].push(`ECDSA em hardware (${seg.ecc}) — atestado de dispositivo Matter assinado sem custo de CPU`);
+ reasons[key].push(`ECDSA em hardware (${seg.ecc}): atestado de dispositivo Matter assinado sem custo de CPU`);
} else if (temECC) {
scores[key] += 10;
reasons[key].push(`Tem ECC (${seg.ecc}) mas não o periférico ECDSA: a assinatura roda parcialmente em software`);
diff --git a/app/series/[key]/page.jsx b/app/series/[key]/page.jsx
index a85deb6..1348e48 100644
--- a/app/series/[key]/page.jsx
+++ b/app/series/[key]/page.jsx
@@ -100,11 +100,11 @@ export default async function SerieDetail({ params }) {
];
const ferramentasSpecs = [
- { label: "ESP-IDF mínimo", value: serie.esp_idf_minimo || "—", icon: },
- { label: "Core Arduino", value: serie.arduino_core || "—", icon: },
- { label: "Depuração", value: serie.depuracao || "—", icon: },
- { label: "Encapsulamento", value: serie.encapsulamento || "—", icon: },
- { label: "Módulos disponíveis", value: serie.modulos || "—", icon: },
+ { label: "ESP-IDF mínimo", value: serie.esp_idf_minimo || "Não informado", icon: },
+ { label: "Core Arduino", value: serie.arduino_core || "Não informado", icon: },
+ { label: "Depuração", value: serie.depuracao || "Não informado", icon: },
+ { label: "Encapsulamento", value: serie.encapsulamento || "Não informado", icon: },
+ { label: "Módulos disponíveis", value: serie.modulos || "Não informado", icon: },
];
const conectividadeSpecs = [
diff --git a/app/series/page.jsx b/app/series/page.jsx
index fd1a275..b93ce9a 100644
--- a/app/series/page.jsx
+++ b/app/series/page.jsx
@@ -126,7 +126,7 @@ export default function Series() {
{String(serie.wifi).includes("Não") ? (
- —
+ Não
) : (
Sim
)}
@@ -140,7 +140,7 @@ export default function Series() {
{serie.bluetooth === "Não" ? (
- —
+ Não
) : (
Sim
)}
diff --git a/app/sobre/page.jsx b/app/sobre/page.jsx
index 3e01c2b..268bb08 100644
--- a/app/sobre/page.jsx
+++ b/app/sobre/page.jsx
@@ -100,7 +100,7 @@ export default function Sobre() {
As informações sobre os microcontroladores ESP32 muitas vezes estão dispersas em datasheets densos,
- manuais de referência (TRM), repositórios secundários e documentação técnica — tudo inteiramente em inglês.
+ manuais de referência (TRM), repositórios secundários e documentação técnica, tudo inteiramente em inglês.
O ESPDocs condensa e estrutura esses dados, auxiliando na triagem rápida antes de aprofundar na engenharia oficial.
diff --git a/components/CodeExample.jsx b/components/CodeExample.jsx
index 7d558b4..cb775d3 100644
--- a/components/CodeExample.jsx
+++ b/components/CodeExample.jsx
@@ -77,7 +77,7 @@ export default function CodeExample({ framework }) {
Demonstração de Código
-
Trecho de inicialização rápida — copie e personalize em seu projeto.
+
Trecho de inicialização rápida. Copie e personalize em seu projeto.
diff --git a/components/PinRestrictions.jsx b/components/PinRestrictions.jsx
index ca1a5bc..0b3c427 100644
--- a/components/PinRestrictions.jsx
+++ b/components/PinRestrictions.jsx
@@ -13,7 +13,7 @@ import {
/**
* Restrições de pinos por série.
*
- * Os grupos são DERIVADOS de public/conexoes/.json — o mesmo arquivo que
+ * Os grupos são DERIVADOS de public/conexoes/.json, o mesmo arquivo que
* alimenta o diagrama de conexões. Nada aqui é escrito à mão, então a seção e o
* diagrama nunca divergem: corrigir um pino no JSON corrige os dois.
*/
@@ -25,7 +25,7 @@ const GRUPOS = [
icone: ArrowDownToLine,
cor: "text-sky-500",
explicacao:
- "Aceitam apenas leitura. Não podem ser configurados como saída e não possuem resistores de pull-up ou pull-down internos — se precisar de um, coloque no circuito.",
+ "Aceitam apenas leitura. Não podem ser configurados como saída e não possuem resistores de pull-up ou pull-down internos. Se precisar de um, coloque no circuito.",
combina: (avisos) => /apenas de entrada|input only/.test(avisos),
},
{
@@ -45,7 +45,7 @@ const GRUPOS = [
icone: Power,
cor: "text-amber-500",
explicacao:
- "O chip lê o nível destes pinos no instante do reset para decidir como inicializar. Dá para usá-los depois do boot, mas o circuito externo não pode forçar o nível errado durante o reset — senão a placa não inicia ou entra em modo de gravação sozinha.",
+ "O chip lê o nível destes pinos no instante do reset para decidir como inicializar. Dá para usá-los depois do boot, mas o circuito externo não pode forçar o nível errado durante o reset, senão a placa não inicia ou entra em modo de gravação sozinha.",
combina: (avisos, cats) =>
cats.includes("Strapping") ||
/strapping|modo de boot|modo de inicialização/.test(avisos),
@@ -65,7 +65,7 @@ const GRUPOS = [
icone: Usb,
cor: "text-teal-500",
explicacao:
- "Saem de fábrica ligados ao USB Serial/JTAG interno. Para usá-los como GPIO é preciso desativar essa função — o que também remove a gravação por USB direto.",
+ "Saem de fábrica ligados ao USB Serial/JTAG interno. Para usá-los como GPIO é preciso desativar essa função, o que também remove a gravação por USB direto.",
combina: (avisos) => /usb serial\/jtag|usb_d[+-]/.test(avisos),
},
{
@@ -118,7 +118,7 @@ export default function PinRestrictions({ conexoes, serieKey, cor }) {
Nem todo pino do {serieKey} está livre para uso geral. {totalPinos} deles têm alguma
- condição — já estão ocupados por memória, são lidos durante o boot ou só funcionam
+ condição: já estão ocupados por memória, são lidos durante o boot ou só funcionam
como entrada. Vale conferir antes de fechar o esquemático.
diff --git a/components/SeriesTabMenu.jsx b/components/SeriesTabMenu.jsx
index 1cd9c2d..63a87be 100644
--- a/components/SeriesTabMenu.jsx
+++ b/components/SeriesTabMenu.jsx
@@ -9,7 +9,7 @@ export default function SeriesTabMenu({ tabs, color }) {
// Troca manual limpa o hash. Sem isso, depois de abrir o diagrama pelo link e
// mudar de aba na mão, o hash continuaria em #connections e um novo clique no
- // link não dispararia hashchange — o botão pareceria quebrado.
+ // link não dispararia hashchange e o botão pareceria quebrado.
const trocarAba = (id) => {
setActiveTab(id);
if (window.location.hash) {
diff --git a/public/apostilas/1/.next/trace b/public/apostilas/1/.next/trace
new file mode 100644
index 0000000..fbb92cd
--- /dev/null
+++ b/public/apostilas/1/.next/trace
@@ -0,0 +1 @@
+[{"name":"generate-buildid","duration":224,"timestamp":13103889101,"id":4,"parentId":1,"tags":{},"startTime":1787229898989,"traceId":"aad4d6d36b5e5e6b"},{"name":"load-custom-routes","duration":309,"timestamp":13103889458,"id":5,"parentId":1,"tags":{},"startTime":1787229898989,"traceId":"aad4d6d36b5e5e6b"},{"name":"next-build","duration":129948,"timestamp":13103762107,"id":1,"tags":{"buildMode":"default","isTurboBuild":"false","version":"15.5.9"},"startTime":1787229898862,"traceId":"aad4d6d36b5e5e6b"}]
diff --git a/public/apostilas/1/esp32-s3-blocos.png b/public/apostilas/1/esp32-s3-blocos.png
new file mode 100644
index 0000000..e556491
Binary files /dev/null and b/public/apostilas/1/esp32-s3-blocos.png differ
diff --git a/public/apostilas/1/index.html b/public/apostilas/1/index.html
new file mode 100644
index 0000000..95e218e
--- /dev/null
+++ b/public/apostilas/1/index.html
@@ -0,0 +1,1863 @@
+
+
+
+
+LASER · Formação em ESP32 · Módulo 1: Conhecendo a Família
+
+
+
+
+
+
+
+
+
+
Paginando a apostila…
+
+
+
+
+
+
+
+
+ Liga Acadêmica de Sistemas Embarcados Centro de Informática · UFPE
+
+
+
Formação em ESP32 · Módulo 1
+
+
Conhecendo a família ESP32
+
+
+ Do silício à bancada: como esses microcontroladores funcionam por dentro, o que cada
+ uma das doze séries faz e não faz, onde eles superam Arduino e STM32, e como escolher
+ o chip certo antes de fechar um projeto.
+
+
+
+
+ Lucas Rayan Guerra
+ Ciência Embarcada · ESPDocs
+
+
+ Material de apoio
+ Uso livre para fins educacionais
+
+
+
+
+
+
+
+
+
+
Apresentação
+
Sobre a formação, sobre a liga, e como aproveitar esta apostila.
+
+
+
+
+ A LASER, Liga Acadêmica de Sistemas Embarcados do Centro de Informática
+ da UFPE tem como propósito fortalecer a área de sistemas embarcados na universidade,
+ promovendo estudo, pesquisa e aplicação de tecnologias que envolvem tanto hardware quanto
+ software. Suas frentes vão de automação e Internet das Coisas a IA na borda, robótica,
+ sistemas críticos, tecnologias veiculares e engenharia biomédica.
+
+
+
+ Esta apostila acompanha a formação em ESP32 da liga. A escolha da plataforma não é
+ acidental: o ESP32 atravessa quase todas as frentes da LASER. Ele é o nó de IoT, é a
+ placa de automação, roda inferência na borda, e é barato o bastante para que cada pessoa
+ tenha o seu.
+
+
+
O que você vai levar deste módulo
+
+
Entender o que existe dentro de um ESP32 e por que ele é diferente de um Arduino.
+
Saber navegar as doze séries da família sem se perder na nomenclatura.
+
Ter um método objetivo para escolher a série de um projeto novo.
+
Reconhecer as limitações reais da plataforma antes de esbarrar nelas.
+
Sair com um sensor real lendo temperatura e umidade na sua bancada.
+
+
+
Pré-requisitos
+
+ Espera-se que você já tenha ligado um LED em algum microcontrolador e saiba o que é uma
+ porta digital. Programação em C ou C++ em nível básico ajuda, mas o código da prática é
+ curto e comentado. Não é preciso saber nada de Wi-Fi, RTOS, RISC-V ou
+ criptografia. Tudo isso aparece explicado no caminho.
+
+
+
Material para a prática
+
+ Uma placa ESP32-S3-DevKitC-1, um sensor DHT11, um
+ resistor de 10 kΩ, jumpers, protoboard e um cabo USB de dados. A lista
+ completa e o esquema de ligação estão no capítulo 9.
+
+
+
Como esta apostila está organizada
+
+ Os capítulos 1 a 3 constroem o entendimento do hardware. Os capítulos 4 a 6 apresentam a
+ família e comparam com alternativas. Os capítulos 7 e 8 são de decisão: como escolher série
+ e ferramenta. O capítulo 9 é a prática, e o 10 fecha com glossário e caminhos de estudo.
+
+
+
+
Ao longo do texto você encontra quatro tipos de caixa:
+
+
+
Dica
+
Atalho, boa prática ou recomendação que economiza tempo.
+
+
+
+
Atenção
+
Ponto que costuma causar problema se passar despercebido.
+
+
+
+
Erro comum
+
Armadilha concreta, com o sintoma que ela produz e como escapar.
+
+
+
+
Curiosidade
+
Contexto ou detalhe técnico que ajuda a fixar o conceito.
+
+
+
+
+
+
+
+
+
Sumário
+
Dez capítulos, do conceito à bancada.
+
+
+
+
+
+
+
+
+
+
1
+
+
O que é o ESP32
+
De onde ele veio e por que virou padrão de fato em projetos conectados.
+
+
+
+
+ O ESP32 é uma linha de microcontroladores da Espressif Systems, empresa
+ fundada em 2008 e sediada em Xangai. A história que interessa começa em 2014, com o
+ ESP8266: um chip barato de Wi-Fi vendido como "módulo serial para internet", que a
+ comunidade descobriu ser, na verdade, um microcontrolador completo por poucos dólares.
+
+
+
+ Em 2016 veio o ESP32 original, já projetado como plataforma: dois núcleos, Wi-Fi,
+ Bluetooth e um conjunto grande de periféricos. A partir de 2020 a linha se ramificou, e
+ hoje são doze séries, cada uma otimizada para um recorte diferente de
+ custo, consumo, conectividade e desempenho.
+
+
+
Linha do tempo
+
+
+
Ano
Lançamento
O que mudou
+
+
2014
ESP8266
Wi-Fi barato ao alcance de qualquer projeto. Ainda muito usado.
+
2016
ESP32
Dois núcleos, Bluetooth e periféricos de verdade. O marco da plataforma.
+
2019
ESP32-S2
Foco em segurança e USB nativo; abre mão do Bluetooth.
+
2020
ESP32-C3
Primeira série RISC-V. Marca a virada de arquitetura da empresa.
+
2021
ESP32-S3
Dois núcleos com instruções vetoriais para IA. Vira o carro-chefe.
+
2022
ESP32-C6, H2
Chega o rádio 802.15.4: Zigbee, Thread e Matter entram na família.
+
2023
ESP32-P4
Alto desempenho sem rádio, com MIPI para câmera e display.
+
2024 e 2025
C5, C61, H4, S31
Wi-Fi 6, dual-band de 5 GHz e a geração seguinte de malha.
+
+
Datas de anúncio. A disponibilidade comercial e o suporte nas ferramentas costumam vir meses depois.
+
+
+
O que explica a adoção
+
+
+ Antes do ESP8266, colocar um projeto na internet significava comprar um microcontrolador
+ e um módulo de rede, ligá-los por serial ou SPI, e gastar boa parte da memória
+ disponível com a pilha de comunicação. O ESP32 traz o rádio dentro do mesmo
+ encapsulamento do processador, com as pilhas de Wi-Fi e Bluetooth prontas,
+ testadas em campo por centenas de milhões de dispositivos.
+
+
+
+ Some a isso o preço, já que um módulo custa menos que muitos microcontroladores sem rádio, e
+ uma documentação pública completa, com datasheets, manuais de referência e código-fonte
+ abertos. Essa combinação é rara no mercado de semicondutores.
+
+
+
SoC, módulo, placa e série: quatro coisas diferentes
+
+
+ Muita confusão nasce de usar "ESP32" para quatro coisas distintas. Vale separar desde já,
+ porque isso muda o que você compra, quanto custa e como projeta:
+
+
+
+
Termo
O que é
Exemplo
+
+
+
Chip (SoC)
+
O silício nu, num encapsulamento QFN de poucos milímetros. Precisa de antena,
+ cristal e memória flash externos para funcionar. É o que a Espressif vende em
+ bobina para fábricas.
+
ESP32-S3
+
+
+
Módulo
+
O chip montado numa plaquinha com flash, cristal, antena, blindagem e, o
+ mais importante, homologação de rádio. É o que vai dentro
+ de um produto comercial.
+
ESP32-S3-WROOM-1
+
+
+
Placa de desenvolvimento
+
O módulo numa placa com regulador de tensão, conector USB, botões de boot e
+ reset e pinos em barra. É o que usamos para aprender e prototipar.
+
ESP32-S3-DevKitC-1
+
+
+
Série
+
A família daquele silício: define a arquitetura do processador, quais rádios
+ existem e quais periféricos estão presentes.
+
Série S3
+
+
+
+
+
+
Homologação pertence ao módulo, não ao chip
+
+ Se um dia você levar um projeto a produto, lembre que as certificações de rádio
+ (Anatel no Brasil, FCC nos Estados Unidos, CE na Europa) são emitidas para o
+ módulo. Usar um módulo já homologado evita um processo de certificação
+ caro e demorado. Projetar com o chip nu significa homologar o seu produto do zero.
+
+
+
+
+
+
+ Os três primeiros níveis numa imagem só. O retângulo prateado no centro é o
+ módulo ESP32-S3-WROOM, com a antena impressa numa ponta e a blindagem
+ metálica cobrindo o resto; dentro dele, sem aparecer, está o chip. Todo
+ o resto (regulador de tensão, conversor USB-serial, botões e as barras de pinos) é a
+ placa de desenvolvimento. Num produto seu, só o módulo iria para a PCB.
+
+
+
+
O que a Espressif entrega junto com o silício
+
+
+ Boa parte do valor do ESP32 não está no chip, e sim no software que vem junto. O
+ ESP-IDF (Espressif IoT Development Framework) é o ambiente oficial:
+ baseado em FreeRTOS, traz as pilhas de Wi-Fi, Bluetooth, TCP/IP, TLS, atualização remota
+ de firmware (OTA), sistemas de arquivos, provisionamento e ferramentas de depuração. É
+ software que levaria anos para uma equipe escrever do zero.
+
+
+
+ Existe também o core Arduino para ESP32, que traduz esse ambiente para as
+ funções que você já conhece. É a porta de entrada, e é o que usaremos na prática, mas por
+ baixo continua sendo o ESP-IDF rodando. Entender isso ajuda quando algo não funciona:
+ muitas respostas estão na documentação do IDF, não na do Arduino.
+
+
+
+
+
+
+
2
+
+
Anatomia: o que existe dentro do chip
+
Os blocos que aparecem em qualquer série, e por que cada um importa.
+
+
+
+
+
+
+ Diagrama de blocos do ESP32-S3, publicado pela Espressif. Vale voltar a ele ao longo da
+ apostila: o Main CPU com os dois núcleos Xtensa e, ao lado, as memórias
+ internas (ROM, SRAM e o cache que discutimos adiante). O bloco
+ RF é a parte analógica do rádio, que fala com a antena. Em
+ RTC mora o coprocessador ULP, que continua acordado com o resto do chip
+ dormindo. E o bloco Security é exatamente o assunto do capítulo 5: SHA,
+ AES, RSA, RNG, HMAC, assinatura digital, Secure Boot e criptografia de flash, tudo em
+ silício.
+
+ Cada série tem o seu diagrama equivalente, com blocos a mais ou a menos. Imagem da
+ Espressif Systems, reproduzida para fins didáticos.
+
+
+
+
Processador: Xtensa ou RISC-V
+
+
+ As séries mais antigas (ESP32, S2, S3) usam núcleos Xtensa, arquitetura
+ licenciada da Cadence. As séries novas (C2, C3, C5, C6, C61, H2, H4, P4, S31) migraram
+ para RISC-V, um conjunto de instruções aberto e livre de royalties.
+
+
+
+ Para quem programa em C, a diferença é quase invisível, porque o compilador cuida disso. Ela
+ aparece quando você precisa de assembly, de ferramentas de análise de baixo nível,
+ ou quando uma biblioteca pré-compilada existe só para uma das arquiteturas. A tendência é
+ clara: todo lançamento recente é RISC-V.
+
+
+
Um ou dois núcleos, e o que fazer com eles
+
+ Séries como ESP32, S3, P4, H4 e S31 têm dois núcleos. No ESP-IDF eles recebem nomes:
+ PRO_CPU (núcleo 0) e APP_CPU (núcleo 1). Por convenção, as
+ tarefas do Wi-Fi e do Bluetooth rodam no núcleo 0, deixando o 1 mais livre para o seu
+ código. É possível fixar uma tarefa em um núcleo específico, recurso valioso quando você
+ precisa de um laço com temporização mais previsível.
+
+
+
O coprocessador de baixo consumo
+
+ Algumas séries trazem um segundo processador de baixíssimo consumo, o ULP
+ ou LP core. Ele continua acordado enquanto o processador principal dorme, lendo um sensor
+ de tempos em tempos e só acordando o resto do chip quando algo interessa. É o que permite
+ um projeto a bateria durar meses em vez de dias.
+
+
+
Memória: quatro tipos, quatro papéis
+
+
+
Memória
Para que serve
Ordem de grandeza
+
+
+
SRAM
+
Memória de trabalho: variáveis, pilha, buffers. Rápida e volátil. É o recurso
+ que acaba primeiro em projetos com Wi-Fi: a pilha de rede sozinha consome
+ dezenas de kilobytes.
+
272 KB a 768 KB
+
+
+
ROM
+
Gravada de fábrica pela Espressif. Contém o bootloader inicial e rotinas
+ básicas. Você não altera, e é por isso que um ESP32 é praticamente
+ impossível de "brickar" de forma permanente.
+
128 KB a 576 KB
+
+
+
Flash externa
+
Onde mora o seu programa. Fica fora do núcleo do chip, num segundo
+ componente ou dentro do mesmo encapsulamento, e conversa por SPI.
+
2 MB a 16 MB típico
+
+
+
PSRAM
+
RAM extra externa, opcional, para quando a SRAM não basta: buffer de câmera,
+ tela gráfica, áudio, modelo de rede neural.
+
2 MB a 32 MB
+
+
+
+
+
+
O detalhe que pega todo mundo
+
+ A flash é externa ao núcleo. Isso significa que alguns pinos do chip
+ já estão ocupados conversando com ela o tempo todo. No ESP32 original, são os GPIO 6 a 11.
+ Se você usar esses pinos para um LED, o chip trava, porque você está interferindo no
+ barramento de onde o programa está sendo lido. Todo ESP32 tem um conjunto assim; o
+ número e a posição mudam conforme a série.
+
+
+
+
+
+
+ O que o diagrama de blocos não deixa evidente: a flash fica fora do
+ chip. O cache e a MMU escondem isso do seu código, mas os seis pinos desse barramento
+ ficam ocupados de forma permanente, mesmo com o seu programa rodando. É daí que sai o
+ primeiro grupo de pinos com restrição, mais adiante neste capítulo. A barra diagonal
+ com o número é a notação usual de esquemático para um feixe de condutores.
+
+
+
+
+
Como o código roda se ele está fora do chip?
+
+ Existe um bloco chamado MMU com cache entre o processador e a flash.
+ Ele traz para dentro do chip os pedaços de código que estão sendo executados, mantendo
+ os mais usados em cache. O efeito é que a flash externa "parece" memória interna, com a ressalva de que um acesso fora do cache custa bem mais caro. Esse é um dos
+ motivos pelos quais o desempenho de um ESP32 varia mais que o de um microcontrolador
+ com flash interna.
+
+
+
+
+
Rádio
+
+
+ É o bloco que distingue o ESP32 da maioria dos microcontroladores. Dependendo da série
+ você encontra:
+
+
+
+
Tecnologia
Para quê
+
+
Wi-Fi 4 (802.11 b/g/n)
Conexão direta à internet pela rede local. Presente na maioria das séries.
+
Wi-Fi 6 (802.11ax)
Melhor desempenho em redes congestionadas e menor consumo com o recurso Target Wake Time.
+
Bluetooth LE
Comunicação com celular, sensores de baixo consumo, provisionamento de rede.
+
Bluetooth Clássico
Áudio (A2DP) e porta serial sem fio (SPP). Só no ESP32 e no S31.
+
IEEE 802.15.4
Base do Zigbee e do Thread, redes em malha de baixo consumo, e portanto do Matter.
+
+
+
+
+
+ O rádio não é de graça: transmitir custa corrente, com picos que passam de 300 mA, ocupa
+ memória e consome tempo de processamento. Voltaremos a isso no capítulo 7.
+
+
+
Periféricos e a matriz de roteamento
+
+
+ GPIO, UART, SPI, I²C, I²S, PWM, ADC, sensor de temperatura interno, USB, controlador CAN
+ (que a Espressif chama de TWAI), contador de pulsos, transceptor de infravermelho. O
+ conjunto varia por série.
+
+
+
+ Uma característica facilita muito a vida: na maioria das séries existe uma
+ matriz de roteamento (GPIO matrix) que permite mapear quase
+ qualquer função para quase qualquer pino, por software. Em um microcontrolador tradicional,
+ a UART2 só existe nos pinos que o fabricante escolheu; aqui, você decide na hora de
+ programar. Na prática do projeto de placa isso é ouro: você desenha o traçado da forma mais
+ limpa e depois adapta o firmware.
+
+
+
+
A matriz tem limite
+
+ Sinais de alta velocidade (o SPI da flash, o USB, o cristal, algumas linhas de I²S e
+ LCD) não passam pela matriz: eles usam IO_MUX, um caminho direto e mais
+ rápido, disponível apenas em pinos específicos. Se você rotear um SPI rápido por um
+ pino qualquer, ele funciona, mas a frequência máxima cai.
+
+
+
+
Pinos com restrição, e o estrago que eles causam
+
+
+ Aqui está a diferença mais concreta entre ler o datasheet e não ler. Num Arduino UNO,
+ os vinte pinos são praticamente equivalentes. Num ESP32, uma parte deles já tem dono, e
+ usar o pino errado não produz um erro de compilação: produz uma placa que não liga, um
+ firmware que trava, ou pior, um protótipo que funciona na bancada e falha em campo.
+
+
+
+ São cinco grupos, e cada um quebra o projeto de um jeito diferente.
+
+
+
+
+
+ A pinagem do ESP32 clássico já traz as restrições marcadas, se você souber o que
+ procurar. As barras vermelhas à esquerda dos GPIO 6 a 11 significam
+ "compartilhado com a memória flash, não pode ser usado como GPIO comum". As etiquetas
+ rosa STRAP marcam os pinos de boot. As setas simples (em vez de duplas)
+ nos GPIO 34 a 39 indicam somente entrada. E as etiquetas
+ DEBUG em azul são as linhas de JTAG.
+
+
+
+
1. Ligados à memória flash ou PSRAM
+
+ Estes pinos conversam com a memória de onde o firmware está sendo lido, o tempo todo,
+ enquanto o chip funciona. Escrever neles interrompe essa conversa no meio.
+
+
+ Como falha: o chip reinicia em laço, ou trava no boot com uma mensagem de
+ erro de leitura da flash. Não é intermitente e não tem contorno em software.
+
+
+ Como evitar: no ESP32-S3 são os GPIO 26 a 32; no ESP32 clássico, os GPIO
+ 6 a 11 (mais o 16 e o 17 em módulos WROVER, que têm PSRAM). Boa notícia: em muitos módulos
+ esses pinos nem chegam à barra de pinos, justamente para você não ter como errar.
+
+
+
2. Strapping (seleção do modo de boot)
+
+ O chip lê o nível destes pinos no instante do reset para decidir como inicializar. Depois
+ do boot eles voltam a ser GPIO normais, e é aí que mora a armadilha: o pino funciona
+ perfeitamente enquanto a placa está ligada, mas o que você pendurou nele atrapalha o
+ próximo reset.
+
+
+ Como falha: a placa para de iniciar quando o circuito externo puxa o pino
+ para o nível errado. Um exemplo comum: você liga um LED com resistor no GPIO0 do ESP32.
+ O LED funciona. Na próxima energização, o resistor mantém o pino em nível baixo, o chip
+ entende que deve entrar em modo de gravação e fica esperando um firmware que nunca chega.
+ A placa parece morta.
+
+
+ Como evitar: reserve os pinos de strapping para saídas que ficam em alta
+ impedância no boot, ou use-os para entradas com o resistor de pull no sentido que o chip
+ espera. Se puder, simplesmente não use.
+
+
+
3. Somente entrada
+
+ Alguns pinos são fisicamente incapazes de acionar uma saída, e não têm resistores de
+ pull-up nem pull-down internos.
+
+
+ Como falha: silenciosamente. O pinMode(pino, OUTPUT) não
+ reclama, o digitalWrite não reclama, e o pino simplesmente não muda de nível.
+ Você passa a tarde procurando o problema no código.
+
+
+ Como evitar: no ESP32 clássico são os GPIO 34, 35, 36, 37, 38 e 39. Use-os
+ para leitura, e lembre de colocar o resistor de pull no circuito, já que não existe um
+ interno para ativar. As séries RISC-V mais novas não têm pinos assim.
+
+
+
4. Gravação e depuração
+
+ São a UART0 (por onde o firmware é gravado e as mensagens de Serial saem) e
+ as quatro linhas de JTAG.
+
+
+ Como falha: você não perde o projeto, perde a capacidade de trabalhar
+ nele. Se um sensor ocupa os pinos da UART0, cada gravação exige desconectar o sensor. Se
+ algo escreve nesses pinos durante a execução, o monitor serial enche de lixo.
+
+
+ Como evitar: deixe a UART0 livre até o projeto estar fechado. Se precisar
+ de mais uma serial, use a UART1 ou a UART2, que a matriz de roteamento leva para quase
+ qualquer pino.
+
+
+
5. USB nativo
+
+ Nas séries com USB no próprio chip, dois pinos saem de fábrica ligados ao periférico
+ USB Serial/JTAG.
+
+
+ Como falha: ao usá-los como GPIO comum, a porta USB de gravação some do
+ computador. Recuperar exige o modo de gravação manual pelos botões BOOT e RESET.
+
+
+ Como evitar: no ESP32-S3 são o GPIO19 e o GPIO20. Trate-os como
+ indisponíveis, a não ser que você tenha certeza de que não vai precisar do USB nativo.
+
+
+
+
Os pinos com restrição do ESP32-S3
+
+
+ Como a prática do capítulo 9 usa um ESP32-S3, vale ter esta lista à mão. Dos 45 GPIOs
+ da série, 18 têm alguma condição:
+
+
+
+
Grupo
Pinos
+
+
Flash e PSRAM
GPIO26, 27, 28, 29, 30, 31 e 32
+
Strapping
GPIO0, GPIO45 e GPIO46
+
Gravação e JTAG
GPIO39, 40, 41, 42 (JTAG) e GPIO43, 44 (UART0)
+
USB nativo
GPIO19 e GPIO20
+
Somente entrada
Nenhum nesta série
+
+
Sobram 27 pinos totalmente livres, incluindo o GPIO12 que usaremos na prática.
+
+
+
+
+
O erro que mais custa caro
+
+ Fechar o esquemático de uma placa própria distribuindo os sinais pelos pinos "que
+ sobraram no traçado", sem conferir a lista. O protótipo chega da fábrica, a placa não
+ liga, e a correção passa por cortar trilha ou mandar refazer o lote.
+
+
+ Cinco minutos de conferência antes do envio valem semanas de prazo. E como a matriz de
+ roteamento deixa quase toda função ir para quase todo pino, quase sempre dá para
+ reorganizar o traçado sem perder nada.
+
+
+
+
+
Onde consultar
+
+ A página de cada série no ESPDocs traz uma seção de pinos com
+ restrição, com os grupos acima listados pino a pino para aquela série específica, mais
+ o diagrama de conexões completo com todas as funções de cada pino.
+ Em espdocs.cienciaembarcada.com.br.
+
+
+
+
+
+
+
+
3
+
+
Como o chip acorda: boot e execução
+
Entender esta sequência economiza horas de depuração.
+
+
+
+
+
+
+
+
+ Quando a gravação falha com timeout, é a etapa 2 que não chegou ao desvio.
+ Segurar BOOT e pressionar RESET força o caminho pontilhado.
+
+
+
+
A sequência de partida
+
+
+
1
+
+
Energização e reset
+
+ Ao receber energia, o chip mantém o processador parado até as tensões estabilizarem.
+ O pino de habilitação (CHIP_EN ou CHIP_PU) precisa estar
+ em nível alto, e é por isso que ele nunca pode ficar flutuando.
+
+
+
+
+
+
2
+
+
Bootloader da ROM
+
+ O processador começa a executar código gravado de fábrica na ROM. Esse código lê o
+ nível dos pinos de strapping para decidir o que fazer: iniciar
+ normalmente a partir da flash, ou entrar em modo de gravação e
+ esperar um firmware chegar pela serial.
+
+
+
+
+
+
3
+
+
Bootloader de segundo estágio
+
+ Se o boot for normal, a ROM carrega da flash um segundo bootloader, este sim
+ gravado por você junto com o firmware. Ele lê a tabela de partições, escolhe qual
+ imagem executar (importante para OTA, onde há duas), verifica assinatura se o
+ Secure Boot estiver ativo, e descriptografa se a flash estiver cifrada.
+
+
+
+
+
+
4
+
+
Sua aplicação
+
+ Só então o seu programa começa. No ESP-IDF, o ponto de entrada é
+ app_main(). No Arduino, o core cria uma tarefa que chama
+ setup() e depois loop() repetidamente. Ou seja: o
+ loop() que você conhece é, por baixo, uma tarefa do FreeRTOS.
+
+
+
+
+
+
Os botões BOOT e RESET
+
+ É por causa do passo 2 que as placas têm esses dois botões. Segurar BOOT
+ enquanto pressiona e solta RESET força o modo de gravação. A maioria
+ das placas modernas faz isso automaticamente pelo conversor USB, mas quando a gravação
+ falha com timeout, esse é o truque manual que resolve.
+
+
+
+
FreeRTOS: seu código não está sozinho
+
+
+ Diferente de um Arduino UNO, onde o seu loop() é literalmente a única coisa
+ executando, no ESP32 há um sistema operacional de tempo real gerenciando
+ várias tarefas ao mesmo tempo: a sua, as do Wi-Fi, as do Bluetooth, o temporizador de
+ sistema, o coletor de estatísticas.
+
+
+
+ O escalonador distribui o processador entre elas por prioridade. Isso traz dois efeitos
+ práticos que surpreendem quem vem do Arduino:
+
+
+
+ Primeiro: um delay() no ESP32 não trava o chip: ele cede o
+ processador para outras tarefas. É por isso que o Wi-Fi continua funcionando enquanto o seu
+ código "espera".
+
+
+
+ Segundo: um laço que nunca cede o processador causa problema. Se você
+ escrever um while(true) sem delay() nem espera, o
+ watchdog vai reiniciar o chip, reclamando que a tarefa não alimentou o cão de
+ guarda. Não é bug: é o sistema protegendo as tarefas do rádio.
+
+
+
+
"Task watchdog got triggered"
+
+ Esta mensagem no monitor serial quase sempre significa laço apertado sem ceder tempo.
+ A correção costuma ser um delay(1) ou um vTaskDelay() dentro
+ do laço. Parece contraintuitivo colocar espera para o programa funcionar, mas em um
+ sistema com RTOS é exatamente o que se faz.
+
+
+
+
Partições: a flash não é um bloco só
+
+
+ A memória flash é dividida segundo uma tabela de partições. Um layout
+ típico tem: o bootloader, a tabela em si, uma área nvs para guardar
+ configurações (como a senha do Wi-Fi), uma ou duas partições de aplicação e, às vezes, um
+ sistema de arquivos.
+
+
+
+ Duas partições de aplicação existem para permitir OTA: o firmware novo é
+ gravado na partição inativa, e só depois de validado o chip passa a iniciar por ela. Se a
+ imagem nova falhar, ele volta sozinho para a anterior. É por isso que um projeto com
+ atualização remota precisa de mais que o dobro do espaço do firmware.
+
+
+
+
+
+
+
4
+
+
A família completa: as 12 séries
+
Cada uma nasceu para resolver um problema diferente.
+
+
+
+
+ A nomenclatura ajuda mais do que parece. A letra indica a vocação da série:
+ S de sensing, desempenho e USB; C de custo
+ reduzido em RISC-V; H de malha 802.15.4 sem Wi-Fi; P de
+ processamento bruto sem rádio.
+
+
+
+
+
+
ESP32
+
O clássico
+
O original de 2016. Dois núcleos, Bluetooth Clássico (raro na família), Ethernet e
+ dois DACs. Ainda é o mais barato e o de maior base instalada.
+
Núcleo
Xtensa LX6 ×2, 240 MHz
+
SRAM
520 KB
GPIO
34
+
+
+
+
ESP32-S2
+
USB e segurança
+
Um núcleo e sem Bluetooth, mas trouxe USB OTG nativo e muitos GPIOs. Bom quando o
+ projeto precisa se comportar como dispositivo USB.
+
Núcleo
Xtensa LX7, 240 MHz
+
SRAM
320 KB
GPIO
43
+
+
+
+
ESP32-S3
+
O cavalo de batalha
+
Dois núcleos, BLE 5, USB nativo, muita RAM e instruções vetoriais para IA. A série
+ mais usada em projetos novos, e a da nossa prática.
+
Núcleo
Xtensa LX7 ×2, 240 MHz
+
SRAM
512 KB
GPIO
45
+
+
+
+
ESP32-C2
+
O mínimo viável
+
Também vendido como ESP8684. Feito para custo: poucos pinos, sem I²S e sem
+ acelerador AES. Para dispositivos simples em altíssimo volume.
+
Núcleo
RISC-V, 120 MHz
+
SRAM
272 KB
GPIO
14
+
+
+
+
ESP32-C3
+
O substituto do clássico
+
O primeiro RISC-V da casa. Wi-Fi, BLE 5 e USB para depuração num chip barato.
+ Escolha natural para trocar um ESP8266 antigo.
+
Núcleo
RISC-V, 160 MHz
+
SRAM
400 KB
GPIO
22
+
+
+
+
ESP32-C6
+
Matter e malha
+
Wi-Fi 6, BLE 6 e 802.15.4 no mesmo chip: fala Zigbee, Thread e Matter. A porta de
+ entrada para automação residencial moderna.
+
Núcleo
RISC-V + LP, 160 MHz
+
SRAM
512 KB
GPIO
30
+
+
+
+
ESP32-C5
+
Wi-Fi de duas faixas
+
O primeiro da família com Wi-Fi 6 em 2,4 GHz e 5 GHz. Útil onde a faixa de
+ 2,4 GHz está congestionada.
+
Núcleo
RISC-V + LP, 240 MHz
+
SRAM
384 KB
GPIO
29
+
+
+
+
ESP32-C61
+
Wi-Fi 6 econômico
+
Wi-Fi 6 com foco em custo, com flash e PSRAM já no encapsulamento. Sem 802.15.4 e
+ sem acelerador AES.
+
Núcleo
RISC-V, 160 MHz
+
SRAM
320 KB
GPIO
30
+
+
+
+
ESP32-H2
+
Malha a bateria
+
Sem Wi-Fi de propósito: só BLE 6 e 802.15.4. Sem o rádio mais faminto, o consumo
+ despenca. Ideal para sensor Zigbee/Thread alimentado por pilha.
+
Núcleo
RISC-V, 96 MHz
+
SRAM
320 KB
GPIO
19
+
+
+
+
ESP32-H4
+
Malha, segunda geração
+
Sucessor do H2, com dois núcleos e BLE 5.4 com LE Audio. Ainda em documentação
+ preliminar, sem datasheet publicado e sem suporte estável.
+
Núcleo
RISC-V ×2, 96 MHz
+
SRAM
384 KB
GPIO
40
+
+
+
+
ESP32-P4
+
Força bruta, sem rádio
+
400 MHz, dois núcleos, MIPI para câmera e display, Ethernet. Não tem Wi-Fi nem
+ Bluetooth: é para interface gráfica e processamento pesado.
+
Núcleo
RISC-V ×2 + LP, 400 MHz
+
SRAM
768 KB
GPIO
55
+
+
+
+
ESP32-S31
+
O topo de linha
+
320 MHz, Wi-Fi 6, Bluetooth Clássico, 802.15.4, CAN FD e aceleração de IA. Ainda
+ sem versão estável do ESP-IDF nem suporte no Arduino.
+
Núcleo
RISC-V ×2 + LP, 320 MHz
+
SRAM
512 KB
GPIO
60
+
+
+
+
+
Tabelas comparativas
+
+
Processamento e memória
+
+
Série
Núcleo
Clock
SRAM
GPIO
ESP-IDF mínimo
+
+
ESP32
Xtensa LX6 ×2
240 MHz
520 KB
34
v3.x
+
ESP32-S2
Xtensa LX7 ×1
240 MHz
320 KB
43
v4.2
+
ESP32-S3
Xtensa LX7 ×2
240 MHz
512 KB
45
v4.4
+
ESP32-S31
RISC-V ×2
320 MHz
512 KB
60
master
+
ESP32-C2
RISC-V ×1
120 MHz
272 KB
14
v5.0
+
ESP32-C3
RISC-V ×1
160 MHz
400 KB
22
v4.3
+
ESP32-C5
RISC-V ×1
240 MHz
384 KB
29
v5.5.2
+
ESP32-C6
RISC-V ×1
160 MHz
512 KB
30
v5.1
+
ESP32-C61
RISC-V ×1
160 MHz
320 KB
30
v5.5.2
+
ESP32-P4
RISC-V ×2
400 MHz
768 KB
55
v5.3
+
ESP32-H2
RISC-V ×1
96 MHz
320 KB
19
v5.1
+
ESP32-H4
RISC-V ×2
96 MHz
384 KB
40
master
+
+
"master" indica série que ainda não entrou em versão estável do ESP-IDF.
+
+
+
Rádios
+
+
Série
Wi-Fi
Bluetooth
802.15.4
Matter
+
+
ESP32
Wi-Fi 4
BLE + Clássico
Não
Não
+
ESP32-S2
Wi-Fi 4
Não
Não
Não
+
ESP32-S3
Wi-Fi 4
BLE 5.4
Não
Não
+
ESP32-S31
Wi-Fi 6
BLE + Clássico
Sim
Sim
+
ESP32-C2
Wi-Fi 4
BLE 5.3
Não
Não
+
ESP32-C3
Wi-Fi 4
BLE 5.4
Não
Não
+
ESP32-C5
Wi-Fi 6 · 2,4 e 5 GHz
BLE 6.0
Sim
Sim
+
ESP32-C6
Wi-Fi 6
BLE 6.0
Sim
Sim
+
ESP32-C61
Wi-Fi 6
BLE 6.0
Não
Via Wi-Fi
+
ESP32-P4
Não
Não
Não
Não
+
ESP32-H2
Não
BLE 6.0
Sim
Sim
+
ESP32-H4
Não
BLE 5.4
Sim
Sim
+
+
+
+
Ferramentas e encapsulamento
+
+
Série
Core Arduino
Depuração
Encapsulamento
+
+
ESP32
147 placas
Sonda externa
QFN48 (6×6 e 5×5 mm)
+
ESP32-S2
26 placas
Sonda externa
QFN56 (7×7 mm)
+
ESP32-S3
158 placas
USB integrado
QFN56 (7×7 mm)
+
ESP32-S31
Não
USB integrado
QFN80 (8×8 mm)
+
ESP32-C2
1 placa
Sonda externa
QFN24 (4×4 mm)
+
ESP32-C3
29 placas
USB integrado
QFN32 (5×5) e QFN28 (4×4 mm)
+
ESP32-C5
10 placas
USB integrado
QFN48 (6×6 mm)
+
ESP32-C6
21 placas
USB integrado
QFN40 e QFN32 (5×5 mm)
+
ESP32-C61
1 placa
USB integrado
QFN40 (5×5 mm)
+
ESP32-P4
7 placas
USB integrado
QFN104 (10×10 mm)
+
ESP32-H2
3 placas
USB integrado
QFN32 (4×4 mm)
+
ESP32-H4
Não
USB integrado
Não divulgado
+
+
Número de definições de placa no core oficial arduino-esp32, um bom termômetro da maturidade do ecossistema.
+
+
+
+
Lançado não é o mesmo que utilizável
+
+ Um chip anunciado pode levar meses até ter suporte estável nas ferramentas. O
+ S31 e o H4 hoje só existem no branch de
+ desenvolvimento do ESP-IDF e não têm suporte no core Arduino. Antes de escolher uma
+ série recente para um projeto com prazo, verifique em qual versão do ESP-IDF ela entrou
+ e se ela já tem módulo à venda.
+
+
+
+
+
+
+
+
5
+
+
Segurança em hardware
+
O que existe no silício, para que serve, e por que isso separa as séries.
+
+
+
+
+ Segurança costuma ser tratada como assunto de software, mas boa parte dela depende de
+ blocos de hardware. Executar criptografia em hardware é dezenas de vezes mais rápido e
+ mais econômico que na CPU. E alguns recursos, como esconder uma chave privada do próprio
+ firmware, são impossíveis sem apoio do silício.
+
+
+
+ Este é também o assunto em que as séries mais divergem, o que faz dele um bom critério de
+ escolha.
+
+
+
Os blocos e o que cada um resolve
+
+
+
Bloco
Para que serve
+
+
AES
Cifra simétrica: é o que protege o tráfego depois que uma conexão TLS é estabelecida.
+
SHA
Funções de resumo (hash). Base de assinaturas, verificação de integridade e HMAC.
+
RSA / MPI
Aritmética de números grandes, usada em chave pública clássica. É o que acelera o handshake TLS.
+
ECC
Criptografia de curvas elípticas: mesma segurança do RSA com chaves muito menores, o que importa bastante em microcontrolador.
+
ECDSA
Assinatura digital sobre curvas elípticas. É o que o Matter exige para o atestado de dispositivo.
+
HMAC
Autenticação de mensagem com chave secreta.
+
Digital Signature
Assina usando uma chave RSA guardada em eFuse, sem que o firmware chegue a vê-la.
+
Key Manager
Provisiona e usa chaves sem que elas fiquem legíveis para o firmware, nem se alguém extrair a imagem.
+
RNG
Gerador de números verdadeiramente aleatórios. Sem ele, toda a criptografia fica previsível.
+
Criptografia de flash
Cifra o conteúdo da flash, impedindo que alguém leia o firmware soldando um gravador na memória.
+
Secure Boot
O chip se recusa a executar firmware que não esteja assinado pela sua chave.
+
Proteção DPA
Defesa contra ataque que deduz a chave medindo variações no consumo de energia durante a operação.
+
+
+
+
O que cada série tem
+
+
+
Série
AES
ECC
ECDSA
Key Manager
Secure Boot
+
+
ESP32
128/192/256
Não
Não
Não
V1 (legado)
+
ESP32-S2
128/192/256 + GCM
Não
Não
Não
V2 · RSA-3072
+
ESP32-S3
128/256
Não
Não
Não
V2 · RSA-3072
+
ESP32-S31
128/256 + GCM
P-192/256/384
Sim
Sim
V2 · RSA-3072
+
ESP32-C2
Não
P-192/256
Não
Não
V2 · ECDSA
+
ESP32-C3
128/256
Não
Não
Não
V2 · RSA-3072
+
ESP32-C5
128/256
P-192/256/384
Sim
Sim
V2 · RSA/ECDSA
+
ESP32-C6
128/256
P-192/256
Não
Não
V2 · RSA/ECDSA
+
ESP32-C61
Não
P-192/256
Sim
Não
V2 · ECDSA
+
ESP32-P4
128/256 + GCM
P-192/256/384
Sim
Sim
V2 · RSA/ECDSA
+
ESP32-H2
128/256
P-192/256
Sim
Não
V2 · RSA/ECDSA
+
ESP32-H4
128/256
P-192/256/384
Sim
Não
Pendente
+
+
Dados extraídos das definições de hardware (soc_caps.h) do ESP-IDF.
+
+
+
Três leituras que essa tabela permite
+
+
+ Nem toda série tem acelerador AES. O C2 e o C61 não têm. Eles conseguem
+ fazer TLS, mas a cifra do tráfego roda na CPU, o que pesa se o dispositivo transmitir
+ muito dado.
+
+
+
+ ECC é um divisor claro. As séries mais antigas (ESP32, S2, S3 e C3) não
+ têm curvas elípticas em hardware. Como o mundo caminha para ECC (chaves menores, menos
+ memória), essa é uma diferença que tende a pesar mais com o tempo.
+
+
+
+ Key Manager é raro. Só S31, C5 e P4. Se o seu produto precisa que a chave
+ privada seja inacessível mesmo para quem tem o firmware em mãos, a lista de candidatos é
+ bem curta.
+
+
+
+
eFuse: memória que só grava uma vez
+
+ Os segredos ficam em eFuses, bits que podem ser queimados uma única
+ vez e nunca mais voltam. É assim que se grava a chave do Secure Boot e da criptografia
+ de flash, e é por isso que essas operações são irreversíveis: uma vez ativado o Secure
+ Boot, aquela placa só executa firmware assinado por você. Para sempre. Em bancada, é
+ uma ótima forma de inutilizar uma placa por engano. Ative apenas quando souber
+ exatamente o que está fazendo.
+
+
+
+
+
+
+
+
6
+
+
ESP32, ESP8266, Arduino e STM32
+
Quatro mundos diferentes, e por que "qual é o melhor" não tem resposta.
+
+
+
+
+ Comparar plataformas exige cuidado: "Arduino" é uma marca de placas e um ambiente de
+ programação, e "STM32" é uma família enorme da STMicroelectronics que vai de chips de
+ centavos a processadores de 480 MHz. Para a comparação fazer sentido, usaremos
+ representantes concretos.
+
+
+
+
+
Arduino UNO R3
ESP8266
ESP32-S3
STM32F103
+
+
+
Núcleo
AVR 8 bits
Xtensa L106
Xtensa LX7 ×2
Cortex-M3
+
Clock
16 MHz
80 a 160 MHz
240 MHz
72 MHz
+
SRAM
2 KB
~50 KB úteis
512 KB
20 KB
+
Flash
32 KB
Externa, 1 a 4 MB
Externa, 4 a 16 MB
64 a 128 KB
+
Wi-Fi
Não
Sim
Sim
Não
+
Bluetooth
Não
Não
BLE 5
Não
+
Tensão de E/S
5 V
3,3 V
3,3 V
3,3 V (alguns 5 V)
+
ADC
10 bits, linear
1 canal, 10 bits
12 bits, calibrar
12 bits, bom
+
GPIO úteis
20
~9
45
37
+
Dormindo
~35 µA
~20 µA
~7 µA
~2 µA
+
+
Valores típicos para ordem de grandeza. Confira sempre o datasheet da peça exata.
+
+
+
Onde cada um ganha
+
+
Arduino UNO
+
+ Ganha em simplicidade e previsibilidade. Sem sistema operacional, sem
+ pilha de rede: o que você escreve é exatamente o que executa, na ordem que escreveu.
+ Trabalha em 5 V, o que dispensa conversão de nível com muitos sensores e módulos antigos.
+ É insuperável como primeiro contato e para ensinar eletrônica. Perde feio em memória, porque 2 KB
+ de SRAM acabam rápido, e não tem qualquer conectividade.
+
+
+
ESP8266
+
+ Ganha em preço puro. Ainda é o jeito mais barato de colocar algo simples
+ no Wi-Fi, e tem uma base gigante de tutoriais. Perde em quase todo o resto: poucos GPIOs
+ utilizáveis, um único canal de ADC, sem Bluetooth, e uma arquitetura que a Espressif já não
+ desenvolve. Para projeto novo, o C3 costuma custar quase o mesmo e é melhor em tudo.
+
+
+
ESP32
+
+ Ganha em conectividade e relação recurso/preço. Você recebe centenas de
+ kilobytes de RAM, dois núcleos, Wi-Fi, Bluetooth e criptografia por hardware pelo preço de
+ um microcontrolador simples. Para qualquer coisa que precise falar com a rede, é difícil
+ justificar outra escolha. Perde em determinismo de tempo real rígido e na qualidade do
+ conversor analógico.
+
+
+
STM32
+
+ Ganha em controle fino e diversidade. A família cobre desde encapsulamentos
+ de 8 pinos até chips com centenas, com variantes especializadas em consumo ultrabaixo
+ (série L), desempenho (H7) ou segurança. Os periféricos de temporização são muito superiores
+ para controle de motor e conversão de potência, e o ecossistema é padrão industrial, com
+ ferramentas de certificação e suporte de longo prazo. Perde na curva de aprendizado, já que a HAL
+ da ST é bem mais verbosa, e não tem rádio integrado, salvo nas séries WB e WL.
+
+
+
+
Regra prática para decidir
+
+ Precisa falar com a internet ou com um celular? ESP32, quase sempre.
+ É controle de tempo real crítico, como motor, inversor ou malha rápida? STM32.
+ É para ensinar eletrônica ou um protótipo simples em 5 V? Arduino.
+ É produto comercial com certificação e vida longa? Avalie STM32 e ESP32
+ juntos, olhando módulos homologados e compromisso de fornecimento.
+
+
+
+
+
"Programar em Arduino" não é o mesmo que "usar um Arduino"
+
+ O ambiente Arduino é só uma camada de software. Você pode programar um ESP32 ou um
+ STM32 usando a IDE do Arduino, e é exatamente o que faremos no capítulo 9. Placa e
+ linguagem são escolhas independentes.
+
+
+
+
+
+
+
+
7
+
+
Pontos fortes e limitações reais
+
O que ninguém conta no vídeo de unboxing.
+
+
+
+
Onde o ESP32 é muito bom
+
+
+ Conectividade sem dor. Subir um cliente MQTT com TLS, um servidor HTTP ou
+ uma conexão BLE são tarefas de dezenas de linhas, com pilhas maduras.
+
+
+ Recursos por real gasto. Nenhuma outra plataforma entrega tanta memória,
+ clock e rádio nessa faixa de preço.
+
+
+ Segurança de verdade. Boot seguro, criptografia de flash e aceleradores
+ criptográficos estão presentes até nas séries baratas, recursos que em outras famílias
+ aparecem só nas linhas premium.
+
+
+ Flexibilidade de pinos. A matriz de roteamento simplifica muito o traçado
+ da placa: você adapta o firmware ao layout, não o contrário.
+
+
+ Atualização remota. OTA com partição dupla e reversão automática vem
+ pronto no framework.
+
+
+ Documentação aberta. Datasheets, manuais de referência, erratas e o
+ código-fonte do framework são públicos e versionados.
+
+
+
Onde ele decepciona
+
+
O conversor analógico é fraco
+
+ É a queixa mais comum e mais justa. O ADC do ESP32 tem não-linearidade perceptível, ruído
+ e variação entre unidades; precisa de calibração para medidas sérias. Pior: em várias
+ séries, o bloco ADC2 fica indisponível enquanto o Wi-Fi está ligado,
+ porque ambos compartilham recursos internos. Se o seu projeto depende de medida analógica
+ precisa, use um ADC externo por I²C ou SPI.
+
+
+
O rádio consome muito
+
+ Picos de transmissão passam de 300 mA. Um regulador subdimensionado ou uma bateria com
+ resistência interna alta provoca reinicializações aparentemente aleatórias, que parecem
+ bug de software e não são.
+
+
+
Tempo real é aproximado
+
+ O firmware roda sobre FreeRTOS, e o rádio tem tarefas de alta prioridade que precisam ser
+ atendidas. Isso introduz variação de alguns microssegundos a milissegundos no seu laço.
+ Existem soluções, como fixar tarefa em um núcleo, usar o periférico RMT ou delegar a
+ hardware dedicado, mas todas exigem planejamento.
+
+
+
Só 3,3 V
+
+ Os pinos não toleram 5 V. Sensor ou display antigo em 5 V exige conversor de nível; ligar
+ direto degrada ou destrói a entrada.
+
+
+
Documentação viva demais
+
+ A família cresce rápido, e com ela erratas, diferenças entre revisões de silício e
+ funcionalidades que mudam de comportamento entre versões do IDF. Ler a errata do seu chip
+ antes de fechar o projeto é hábito recomendado.
+
+
+
Consumo em repouso não é mágico
+
+ Os 7 µA de deep sleep que aparecem no datasheet valem para o chip nu, em
+ condições ideais. Numa placa de desenvolvimento, o regulador, o LED de energia e o
+ conversor USB-serial costumam consumir muito mais que o próprio ESP32. Medir consumo em
+ DevKit leva a conclusões erradas.
+
+
+
+
Erro clássico de bancada
+
+ Alimentar a placa pela USB do notebook e ligar nela um motor, uma fita de LED ou um
+ display grande. Na hora em que o Wi-Fi transmite, a corrente ultrapassa o que a porta
+ fornece, a tensão cai e o chip reinicia. O sintoma parece software travando; a causa é
+ alimentação. Use fonte externa adequada para as cargas.
+
+
+
+
+
+
+
+
8
+
+
Como escolher a série do seu projeto
+
Um método em cinco perguntas, na ordem que elimina mais opções primeiro.
+
+
+
+
+ A escolha fica muito mais simples quando feita na ordem certa. Comece pelo que é impossível
+ contornar depois, que é o rádio, e só no fim olhe para o que é ajustável.
+
+
+
Pergunta 1: Como ele se comunica?
+
+ Esta pergunta sozinha elimina a maior parte da família, porque rádio não se acrescenta
+ depois.
+
+
+
+
Necessidade
Séries que atendem
+
+
Wi-Fi + Bluetooth LE
ESP32, S3, C3, C6, C5, C61, S31
+
Bluetooth Clássico (áudio, SPP)
ESP32, S31
+
Zigbee / Thread / Matter em malha
C6, C5, H2, H4, S31
+
Wi-Fi 5 GHz
C5
+
Ethernet cabeada
ESP32, P4
+
Sem rádio, só processamento
P4
+
+
+
+
Pergunta 2: De onde vem a energia?
+
+ Se o projeto vive de bateria ou painel solar, o consumo em repouso passa a mandar em tudo.
+ Séries com processador de baixo consumo (C5, C6, P4, S31) mantêm uma tarefa simples rodando
+ com o resto do chip dormindo. E se você não precisa de Wi-Fi, o H2 e o H4 economizam
+ justamente por não carregarem o rádio mais faminto.
+
+
+
Pergunta 3: O que vai ligado nele?
+
+ Conte os pinos de que precisa e verifique as interfaces especiais. Display gráfico grande
+ ou câmera pedem S3 ou P4 (que tem MIPI). Muitos atuadores pedem uma série com bastante
+ GPIO, lembrando de descontar os pinos com restrição. USB nativo, para o dispositivo se
+ apresentar como teclado ou pendrive, existe do S2 em diante e nas séries C recentes.
+
+
+
Pergunta 4: Que exigência de segurança existe?
+
+ Para um protótipo, qualquer série serve. Para produto, o requisito define o chip: atestado
+ de dispositivo Matter pede ECDSA em hardware; chave privada que o firmware nunca pode ler
+ pede Key Manager. Consulte a tabela do capítulo 5.
+
+
+
Pergunta 5: Quando precisa estar pronto?
+
+ Esta é a pergunta esquecida. Uma série recém-anunciada pode não ter suporte estável no
+ ESP-IDF, não ter core Arduino, não ter módulo à venda e não ter placa de desenvolvimento
+ acessível. Para trabalho com prazo, prefira séries com pelo menos um ano de estrada.
+
+
+
Atalhos por perfil de projeto
+
+
+
Se o seu projeto é…
Comece por
Por quê
+
+
Primeiro projeto, aprendendo
ESP32-S3
Muita RAM, USB nativo para depurar e o maior ecossistema de exemplos
+
Sensor Wi-Fi simples e barato
ESP32-C3
Faz o essencial pelo menor preço, com boa maturidade
+
Automação residencial / Matter
ESP32-C6
Único caminho maduro com Wi-Fi 6 e 802.15.4 juntos
+
Sensor a pilha, sem Wi-Fi
ESP32-H2
Sem o rádio mais custoso, a autonomia muda de patamar
+
Tela, câmera ou interface gráfica
ESP32-P4 ou S3
MIPI e RAM suficiente para buffer de vídeo
+
Áudio por Bluetooth Clássico
ESP32
Praticamente a única série com BR/EDR
+
Visão computacional na borda
ESP32-S3
Instruções vetoriais e PSRAM para o buffer da câmera
+
Produto em altíssimo volume
ESP32-C2
O menor custo da família, se os requisitos couberem nele
+
+
+
+
ESP-IDF ou Arduino?
+
+
+ Escolhida a série, falta escolher a ferramenta. As duas são oficiais e mantidas pela
+ Espressif; a diferença é o nível de controle.
+
+
+
+
Core Arduino
ESP-IDF
+
+
Curva de entrada
Baixa, familiar
Íngreme: menuconfig, CMake, componentes
+
Controle do hardware
Parcial
Total
+
Acesso a recursos novos
Chega depois
Imediato
+
Bibliotecas de sensores
Enorme
Menor, mais técnica
+
Consumo de memória
Maior
Ajustável
+
Indicado para
Aprender, prototipar
Produto, otimização, recursos avançados
+
+
+
+
+
Não é escolha definitiva
+
+ Dá para usar o core Arduino como componente dentro de um projeto ESP-IDF,
+ misturando os dois. Um caminho comum é começar no Arduino para validar a ideia e migrar
+ para o IDF quando aparecer a necessidade de afinar consumo, segurança ou tempo de boot.
+
+
+
+
+
Ferramenta de apoio
+
+ O ESPDocs tem um seletor que faz exatamente estas perguntas e devolve
+ uma recomendação justificada, além de comparar até quatro séries lado a lado, com
+ pinagem interativa e ficha de segurança.
+ Em espdocs.cienciaembarcada.com.br.
+
+
+
+
+
+
+
+
9
+
+
Prática: lendo um DHT11 com o ESP32-S3
+
Do jumper ao terminal mostrando temperatura e umidade.
+
+
+
+
+ Vamos montar o "olá mundo" dos sensores: ler temperatura e umidade do ambiente e imprimir
+ no monitor serial. O DHT11 é barato, comum e usa um protocolo digital próprio de um único
+ fio, o que já ensina algo além de acender um LED.
+
+
+
Lista de material
+
+
+
Item
Qtd.
Observação
+
+
ESP32-S3-DevKitC-1
1
Qualquer variante de flash/PSRAM serve
+
Sensor DHT11
1
O componente azul de 4 pinos
+
Resistor de 10 kΩ
1
Pull-up do dado (marrom, preto, laranja)
+
Protoboard
1
Meia protoboard basta
+
Jumpers
4
Vermelho, preto e amarelo, por convenção
+
Cabo USB
1
De dados. Cabo só de carga não funciona
+
+
+
+
Montagem
+
+
+
Ligação
Cor na figura
Função
+
+
VCC do DHT11 → 3V3
Vermelho
Alimentação do sensor
+
DATA do DHT11 → GPIO12
Amarelo
Linha de dados bidirecional
+
GND do DHT11 → GND
Preto
Referência de terra
+
10 kΩ entre DATA e 3V3
Resistor
Mantém a linha em nível alto em repouso
+
+
O terceiro pino do DHT11 (contando da esquerda) não é usado.
+
+
+
+
+
+ Ligação completa. O fio amarelo leva o dado ao GPIO12; o resistor de
+ 10 kΩ conecta essa mesma linha ao 3V3, funcionando como pull-up. Repare que a
+ placa tem duas portas USB na borda inferior, marcadas UART e USB. A
+ diferença entre elas importa e está explicada no passo 4.
+
+ A linha de dados do DHT11 é bidirecional: ora o ESP32 fala, ora o
+ sensor responde. Para isso funcionar sem que os dois disputem a linha, ambos apenas
+ "puxam para baixo" quando querem enviar um zero, e soltam a linha quando querem enviar
+ um um. Só que uma linha solta não tem nível definido: ela flutua e capta ruído.
+
+
+ O resistor de 10 kΩ resolve isso mantendo a linha em nível alto em repouso, com força
+ suficiente para definir o nível, mas fraca o bastante para que qualquer um dos dois
+ consiga puxá-la para baixo. Módulos DHT11 já montados em plaquinha trazem esse resistor
+ embutido; o componente azul sozinho, não.
+
+
+
+
+
Como o sensor conversa
+
+
+ Vale entender o que a biblioteca faz por você. A cada leitura acontece esta troca, toda
+ por um único fio:
+
+
+
+
+
+ O mesmo fio serve para os dois lados conversarem, em momentos diferentes. Um zero e um
+ um se distinguem só pela duração do nível alto, e é por isso que a
+ leitura falha quando o pull-up está ausente: sem ele a linha não sobe rápido o bastante
+ e os tempos saem errados.
+
+
+
+
+
+
O ESP32 puxa a linha para baixo por cerca de 18 ms, que é o "acorda" do sensor.
+
Solta a linha; o pull-up a leva para alto.
+
O DHT11 responde puxando para baixo por 80 µs e soltando por mais 80 µs.
+
Em seguida envia 40 bits: umidade (2 bytes), temperatura (2 bytes) e
+ um byte de verificação.
+
Cada bit é codificado pela duração do nível alto: cerca de 26 µs é zero,
+ cerca de 70 µs é um.
+
+
+
+ A biblioteca mede esses tempos e confere o byte de verificação. Se a soma não bater,
+ seja por ruído, fio solto ou falta de pull-up, ela devolve NaN, e é isso que o
+ nosso código testa.
+
+
+
Passo a passo
+
+
+
1
+
+
Instale o suporte ao ESP32 na IDE
+
+ Na Arduino IDE, abra Arquivo → Preferências e adicione em "URLs adicionais
+ para gerenciadores de placas":
+
+ Depois vá em Ferramentas → Placa → Gerenciador de placas, procure por
+ esp32 e instale o pacote da Espressif Systems. São alguns minutos
+ de download.
+
+
+
+
+
+
2
+
+
Instale a biblioteca do sensor
+
+ Em Ferramentas → Gerenciar bibliotecas, procure por
+ DHT sensor library, da Adafruit, e instale. A IDE vai perguntar se
+ quer instalar as dependências. Aceite, porque ela precisa da
+ Adafruit Unified Sensor junto.
+
+
+
+
+
+
3
+
+
Escreva o programa
+
#include<DHT.h>
+
+#define PINO_DHT 12// fio amarelo
+#define TIPO_DHT DHT11
+
+DHT dht(PINO_DHT, TIPO_DHT);
+
+voidsetup() {
+ Serial.begin(115200);
+ delay(1000); // tempo para a USB enumerar
+ dht.begin();
+ Serial.println("Leitor DHT11 - ESP32-S3");
+}
+
+voidloop() {
+ float umidade = dht.readHumidity();
+ float temperatura = dht.readTemperature();
+
+ if (isnan(umidade) || isnan(temperatura)) {
+ Serial.println("Falha na leitura do sensor");
+ } else {
+ Serial.printf("Temperatura: %.1f C | Umidade: %.1f %%\n",
+ temperatura, umidade);
+ }
+
+ delay(2000); // DHT11 nao aceita leitura mais rapida
+}
+
+ O delay(2000) não é enfeite: o DHT11 leva cerca de um segundo para
+ fazer uma medição e responde com erro se for consultado antes disso.
+
+
+
+
+
+
4
+
+
Configure a placa: o passo onde todo mundo tropeça
+
+ Em Ferramentas, selecione a placa ESP32S3 Dev Module.
+ Agora, o detalhe importante: o DevKitC tem duas portas USB.
+
+
+ A porta marcada UART passa por um conversor USB-serial e funciona
+ como em qualquer Arduino. A porta marcada USB é o USB nativo do
+ próprio ESP32-S3.
+
+
+ Se você ligar o cabo na porta USB, precisa habilitar
+ USB CDC On Boot → Enabled, senão o monitor serial fica mudo mesmo com o
+ programa rodando. Ligando na porta UART, deixe essa opção como
+ Disabled.
+
+
+
+
+
+
5
+
+
Grave e observe
+
+ Selecione a porta em Ferramentas → Porta, clique em carregar e abra o
+ monitor serial em 115200 baud. Depois de alguns segundos:
+
+
+
Leitor DHT11 - ESP32-S3
+Temperatura: 26.0 C | Umidade: 61.0 %
+Temperatura: 26.0 C | Umidade: 61.0 %
+Temperatura: 26.1 C | Umidade: 62.0 %
+
+ Segure o sensor entre os dedos e sopre nele: a umidade sobe rápido e a temperatura
+ acompanha devagar. Essa diferença de velocidade é real: o elemento de umidade
+ responde muito mais rápido que o termistor.
+
+
+
+
+
Quando não funciona
+
+
+
Sintoma
Causa provável e solução
+
+
+
Monitor serial vazio
+
Cabo na porta USB nativa sem USB CDC On Boot habilitado. Veja o passo
+ 4. Confira também a velocidade em 115200.
+
+
+
"Falha na leitura" sempre
+
Fio de dados no pino errado, VCC não conectado, ou resistor de pull-up ausente
+ ou mal encaixado na protoboard.
+
+
+
Leitura falha às vezes
+
Jumper com mau contato, fio de dados muito longo, ou delay menor
+ que 2 segundos.
+
+
+
A porta não aparece
+
Cabo só de carga (troque por um de dados) ou driver do conversor USB-serial
+ faltando.
+
+
+
Erro ao gravar, timeout
+
Placa não entrou em modo de gravação: segure BOOT, pressione e solte RESET,
+ solte BOOT e tente de novo.
+
+
+
Temperatura sempre inteira
+
Não é defeito. O DHT11 tem resolução de 1 °C. O DHT22 dá 0,1 °C.
+
+
+
Valores absurdos e constantes
+
Sensor invertido na alimentação. Confira VCC e GND antes de energizar de novo.
+
+
+
+
+
+
Por que o GPIO12?
+
+ No ESP32-S3, o GPIO12 é um pino comum: não participa do boot, não conversa com a flash
+ e não é usado para depuração. Ele até tem funções alternativas de touch e ADC2, mas
+ nenhuma delas atrapalha o uso como entrada e saída digital.
+
+
+ Em outras séries, cuidado: no ESP32 original o GPIO12 é pino de strapping:
+ ele seleciona a tensão da flash, e um nível alto durante o reset impede a placa de
+ iniciar. Mesmo número de pino, comportamento completamente diferente.
+
+
+ Se você montasse este mesmo circuito num ESP32 clássico sem trocar o pino, o resistor
+ de pull-up de 10 kΩ que o DHT11 exige seguraria o GPIO12 em nível alto exatamente
+ durante o reset. A placa simplesmente não iniciaria, e o sintoma não apontaria para o
+ sensor em momento algum. É o grupo 2 do capítulo 2 acontecendo na prática.
+
+
+
+
Desafios
+
+
Se sobrar tempo na formação, ou para praticar depois:
+
+
+
Acenda o LED RGB da placa em vermelho quando a temperatura passar de 28 °C e em azul
+ abaixo disso.
+
Calcule e imprima a média das últimas dez leituras, para suavizar o ruído.
+
Troque o delay por controle de tempo com millis(), de modo
+ que o programa continue respondendo enquanto espera.
+
Detecte e conte quantas vezes a umidade subiu mais de 5 % entre leituras consecutivas.
+
Publique a leitura numa página web servida pelo próprio ESP32, acessível por qualquer
+ aparelho na mesma rede.
+
+
+
+
+
+
+
10
+
+
Glossário e próximos passos
+
Os termos que aparecem o tempo todo, e para onde seguir.
+
+
+
+
Glossário
+
+
+
Termo
Significado
+
+
SoC
System on Chip: um único chip reunindo processador, memória, rádio e periféricos.
+
GPIO
Pino de entrada e saída de uso geral.
+
Strapping
Pino lido no reset para definir o modo de boot.
+
Pull-up / pull-down
Resistor que define o nível de uma linha em repouso.
+
ULP / LP core
Coprocessador de baixíssimo consumo que roda com o chip principal dormindo.
+
PSRAM
Memória RAM externa opcional, para buffers grandes.
+
eFuse
Memória que só pode ser gravada uma vez, usada para chaves e configurações permanentes.
+
Deep sleep
Modo em que quase tudo desliga, mantendo só o mínimo para acordar depois.
+
OTA
Over-The-Air: atualização de firmware pela rede.
+
FreeRTOS
Sistema operacional de tempo real que gerencia as tarefas do ESP32.
+
Watchdog
Temporizador que reinicia o chip se uma tarefa travar.
+
TWAI
Nome que a Espressif dá ao controlador compatível com CAN.
+
Matter
Padrão de interoperabilidade para casa conectada, sobre Wi-Fi ou Thread.
+
Thread / Zigbee
Protocolos de rede em malha de baixo consumo sobre IEEE 802.15.4.
+
Secure Boot
Mecanismo que impede o chip de executar firmware não assinado.
+
Errata
Documento com os defeitos conhecidos de uma revisão do silício.
Especificações, pinagem, segurança e comparação das séries em português. Em espdocs.cienciaembarcada.com.br
+
Datasheet da série
Verdade final sobre limites elétricos, pinagem e encapsulamento
+
Technical Reference Manual
Detalhe de registradores e comportamento de cada periférico
+
ESP-IDF Programming Guide
API oficial, exemplos e guias de Wi-Fi, BLE, OTA e segurança
+
Errata do chip
Defeitos conhecidos daquela revisão. Leia antes de fechar projeto
+
Fórum esp32.com
Dúvidas específicas, com participação de engenheiros da Espressif
+
GitHub da Espressif
Código-fonte, exemplos e issues abertas
+
+
+
+
Por onde continuar
+
+ Com o sensor funcionando, o caminho natural é tirar o dado da bancada. Na ordem que
+ costuma render mais aprendizado por hora investida:
+
+
+
+
Conectar ao Wi-Fi e servir uma página com a leitura. É o exercício que
+ mais amarra os conceitos: rede, memória e o laço principal disputando espaço.
+
Publicar em um broker MQTT, que é o protocolo padrão de IoT e o que
+ permite o dado chegar a um painel ou a outro dispositivo.
+
Economizar energia com deep sleep, medindo de verdade quanto tempo uma
+ bateria aguenta.
+
Atualizar o firmware pelo ar com OTA, fechando o ciclo de um produto
+ que não precisa mais de cabo depois de instalado.
+
Migrar um projeto para o ESP-IDF, para enxergar o que o core Arduino
+ estava escondendo.
+
+
+
+
Um conselho para levar
+
+ Quando algo não funcionar, resista à vontade de trocar linhas de código no chute.
+ Pergunte antes: o hardware está certo? A alimentação aguenta? Esse pino tem restrição?
+ A biblioteca é a certa para esta série? A maior parte dos problemas em sistemas
+ embarcados não está no algoritmo, e sim na fronteira entre o software e o mundo físico.
+
+
+
+
+ Material produzido para a LASER, Liga Acadêmica de Sistemas Embarcados do Centro de Informática
+ da UFPE. Dados técnicos conferidos nos datasheets oficiais da
+ Espressif e nas definições de hardware do ESP-IDF. Este é um material independente, sem
+ vínculo com a Espressif Systems.
+
+
+
+
+
+
+
diff --git a/public/apostilas/1/pratica.fzz b/public/apostilas/1/pratica.fzz
new file mode 100644
index 0000000..faa29fd
Binary files /dev/null and b/public/apostilas/1/pratica.fzz differ
diff --git a/public/apostilas/1/pratica.png b/public/apostilas/1/pratica.png
new file mode 100644
index 0000000..b0f7ba7
Binary files /dev/null and b/public/apostilas/1/pratica.png differ
diff --git a/public/apostilas/assets/apostila.css b/public/apostilas/assets/apostila.css
new file mode 100644
index 0000000..4243041
--- /dev/null
+++ b/public/apostilas/assets/apostila.css
@@ -0,0 +1,670 @@
+/* =========================================================================
+ Sistema visual das apostilas da LASER, compartilhado por todas as edições.
+ Paginação A4 via Paged.js. Identidade da liga: prussiano, âmbar, laranja e
+ vermelho, tirados da logo oficial (laser.cin.ufpe.br).
+
+ Uma apostila nova só precisa de:
+
+
+ e usar as classes documentadas aqui.
+ ========================================================================= */
+
+:root {
+ /* Paleta da LASER, extraída da logo oficial (laser.cin.ufpe.br).
+ O gradiente é o próprio "feixe" da marca. */
+ --prussiano: #003049;
+ --vermelho: #D62828;
+ --laranja: #F77F00;
+ --ambar: #FCBF49;
+ --creme: #EAE2B7;
+ /* O laranja da marca é vibrante demais para texto: 2,63:1 sobre branco.
+ Esta versão queimada mantém a matiz e passa em AA (4,74:1). */
+ --laranja-texto: #B85800;
+
+ --tinta: #062639;
+ --tinta-2: #33566a;
+ --tinta-3: #557080; /* 5,23:1 no branco e 4,94:1 na linha zebrada */
+ --linha: #c2d2db;
+ --linha-suave: #dde7ed;
+ --fundo-suave: #f5f9fb;
+
+ /* O feixe da marca. Use APENAS em superfície grande e decorativa: uma faixa,
+ um traço largo, o desenho da capa. Nunca recortado em texto: cinco paradas
+ de cor num glifo viram arco-íris e ainda quebram o contraste. */
+ --marca: linear-gradient(90deg, var(--prussiano), var(--creme), var(--ambar), var(--laranja), var(--vermelho));
+}
+
+/* -------------------------------------------------------------------------
+ 1. Página A4
+ ------------------------------------------------------------------------- */
+
+@page {
+ size: A4;
+ margin: 20mm 18mm 18mm 18mm;
+
+ @bottom-left {
+ content: string(titulo-corrente);
+ font-family: "Inter", sans-serif;
+ font-size: 7.5pt;
+ color: #94a3b8;
+ padding-bottom: 4mm;
+ }
+
+ @bottom-right {
+ content: counter(page);
+ font-family: "Outfit", sans-serif;
+ font-size: 9pt;
+ font-weight: 700;
+ color: #64748b;
+ padding-bottom: 4mm;
+ }
+}
+
+/* Capa e contracapa sem cabeçalho, rodapé ou margem. */
+@page capa {
+ margin: 0;
+ @bottom-left { content: none; }
+ @bottom-right { content: none; }
+}
+
+.pagina-capa {
+ page: capa;
+ /* Sem break-after: todo .capitulo já traz break-before, e as duas juntas
+ podem gerar uma página em branco depois da capa. */
+}
+
+/* Capa: clara e minimalista. O desenho é o feixe da LASER atravessando
+ a página. Conceitual, não decorativo. */
+.capa {
+ width: 210mm;
+ height: 297mm;
+ background: #fdfdfb;
+ color: var(--tinta);
+ display: flex;
+ flex-direction: column;
+ padding: 30mm 24mm 24mm;
+ position: relative;
+ overflow: hidden;
+}
+
+.capa-curvas {
+ position: absolute;
+ inset: 0;
+ width: 210mm;
+ height: 297mm;
+ pointer-events: none;
+}
+
+/* O desenho fica no fundo; todo o resto sobe acima dele.
+ Precisa excluir .capa-curvas: mesma especificidade sobrescreveria o absolute. */
+.capa > *:not(.capa-curvas) { position: relative; z-index: 1; }
+
+.capa-marca {
+ display: flex;
+ align-items: center;
+ gap: 4mm;
+ margin-bottom: 26mm;
+}
+
+.capa-marca img { height: 9mm; width: auto; }
+
+.capa-marca span {
+ font-size: 8pt;
+ line-height: 1.35;
+ color: var(--tinta-3);
+ border-left: 0.6pt solid var(--linha);
+ padding-left: 4mm;
+}
+
+.capa-selo {
+ font-family: "Outfit", sans-serif;
+ font-size: 8pt;
+ font-weight: 600;
+ letter-spacing: 0.22em;
+ text-transform: uppercase;
+ color: var(--laranja-texto);
+ margin-bottom: 5mm;
+}
+
+.capa-titulo {
+ font-family: "Outfit", sans-serif;
+ font-size: 42pt;
+ font-weight: 300;
+ line-height: 1.02;
+ letter-spacing: -0.025em;
+ margin: 0 0 6mm;
+ color: var(--prussiano);
+ max-width: 140mm;
+}
+
+.capa-titulo strong { font-weight: 700; }
+
+.capa-sub {
+ font-size: 11pt;
+ font-weight: 300;
+ line-height: 1.55;
+ color: var(--tinta-2);
+ max-width: 116mm;
+ text-align: left;
+ margin-bottom: 0;
+}
+
+.capa-rodape {
+ margin-top: auto;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+ gap: 8mm;
+ font-size: 8pt;
+ color: var(--tinta-3);
+ line-height: 1.6;
+ border-top: 0.6pt solid var(--linha);
+ padding-top: 5mm;
+}
+
+.capa-rodape strong {
+ color: var(--tinta);
+ font-weight: 600;
+ display: block;
+ font-size: 9pt;
+ margin-bottom: 0.8mm;
+}
+
+/* -------------------------------------------------------------------------
+ 2. Base tipográfica
+ ------------------------------------------------------------------------- */
+
+* { box-sizing: border-box; }
+
+html {
+ /* Faz fundos e cores chegarem ao PDF em vez de imprimirem em branco. */
+ -webkit-print-color-adjust: exact;
+ print-color-adjust: exact;
+}
+
+body {
+ margin: 0;
+ font-family: "Inter", system-ui, -apple-system, sans-serif;
+ font-size: 10pt;
+ line-height: 1.62;
+ color: var(--tinta-2);
+ hyphens: auto;
+}
+
+p {
+ margin: 0 0 3.2mm;
+ text-align: justify;
+ /* Nunca deixa uma ou duas linhas soltas no pé nem no topo da página. */
+ orphans: 3;
+ widows: 3;
+}
+strong { color: var(--tinta); font-weight: 600; }
+
+a { color: #b34700; text-decoration: none; border-bottom: 0.6pt solid #f7c9a0; }
+
+ul, ol { margin: 0 0 3.5mm; padding-left: 5.5mm; }
+li { margin-bottom: 1.4mm; orphans: 2; widows: 2; }
+li::marker { color: var(--laranja-texto); }
+
+/* -------------------------------------------------------------------------
+ 3. Capítulos e títulos
+ ------------------------------------------------------------------------- */
+
+.capitulo { break-before: page; }
+
+.capitulo-abertura {
+ display: flex;
+ align-items: flex-start;
+ gap: 5mm;
+ margin-bottom: 7mm;
+ padding-bottom: 4mm;
+ border-bottom: 2.2pt solid var(--tinta);
+}
+
+.capitulo-numero {
+ font-family: "Outfit", sans-serif;
+ font-size: 44pt;
+ font-weight: 300; /* leve, para não competir com o título em 800 */
+ line-height: 0.8;
+ letter-spacing: -0.03em;
+ color: var(--laranja-texto);
+ flex-shrink: 0;
+ font-variant-numeric: lining-nums;
+}
+
+h1, h2, h3, h4 {
+ font-family: "Outfit", sans-serif;
+ color: var(--tinta);
+ margin: 0;
+ break-after: avoid;
+ text-align: left;
+}
+
+h2 {
+ font-size: 19pt;
+ font-weight: 800;
+ line-height: 1.1;
+ letter-spacing: -0.01em;
+ /* Alimenta o rodapé com o capítulo corrente. */
+ string-set: titulo-corrente content();
+}
+
+.capitulo-abertura .subtitulo {
+ font-family: "Inter", sans-serif;
+ font-size: 9pt;
+ font-weight: 400;
+ color: var(--tinta-3);
+ margin-top: 1.5mm;
+ line-height: 1.45;
+}
+
+h3 {
+ font-size: 12.5pt;
+ break-before: auto;
+ font-weight: 700;
+ margin: 7mm 0 2.5mm;
+ padding-left: 3mm;
+ border-left: 2.5pt solid var(--laranja);
+}
+
+h4 {
+ font-size: 10pt;
+ font-weight: 700;
+ margin: 5mm 0 2mm;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: var(--tinta-3);
+}
+
+/* -------------------------------------------------------------------------
+ 3.1. Coesão entre blocos
+
+ O fragmentador quebra onde couber, e o resultado padrão separa coisas que
+ pertencem juntas: a frase que apresenta uma tabela numa página e a tabela na
+ seguinte. Estas duas classes resolvem, e devem ser usadas sempre que um
+ trecho só faz sentido lido de uma vez.
+ ------------------------------------------------------------------------- */
+
+/* Na frase que apresenta o que vem logo abaixo. Proíbe quebra logo depois dela. */
+.junto { break-after: avoid; }
+
+/* NÃO declare break-before:avoid de forma genérica (h3 + *, .junto + *).
+ Testado: o Paged.js encadeia as recusas e produz páginas com 2% de conteúdo,
+ além de partir palavras entre páginas. Para manter um título junto do que ele
+ apresenta, envolva os dois em .grupo, que usa break-inside:avoid, o único
+ controle que o fragmentador aplica de forma previsível. */
+
+/* No wrapper de um conjunto curto que precisa caber inteiro na mesma página
+ (uma introdução mais os itens que ela apresenta, por exemplo). */
+.grupo { break-inside: avoid; }
+
+/* -------------------------------------------------------------------------
+ 4. Caixas de destaque
+ Cada tipo tem um papel didático fixo. Não misture.
+ ------------------------------------------------------------------------- */
+
+.caixa {
+ break-inside: avoid;
+ margin: 4.5mm 0;
+ padding: 3.5mm 4mm;
+ border-radius: 2mm;
+ border-left: 2.5pt solid var(--cor-caixa, var(--laranja));
+ background: var(--fundo-caixa, #fff8ec);
+ font-size: 9pt;
+ line-height: 1.55;
+}
+
+.caixa p { margin-bottom: 2mm; text-align: left; }
+.caixa p:last-child { margin-bottom: 0; }
+
+.caixa-titulo {
+ font-family: "Outfit", sans-serif;
+ font-weight: 800;
+ font-size: 8.5pt;
+ text-transform: uppercase;
+ letter-spacing: 0.07em;
+ color: var(--cor-caixa, var(--laranja));
+ margin-bottom: 1.5mm;
+}
+
+.caixa.atencao { --cor-caixa: #a35a00; --fundo-caixa: #fff8ea; }
+.caixa.erro { --cor-caixa: #b01f1f; --fundo-caixa: #fdf1f1; }
+.caixa.dica { --cor-caixa: #0b6b52; --fundo-caixa: #eefaf5; }
+.caixa.curiosidade { --cor-caixa: #003049; --fundo-caixa: #eff6fa; }
+
+/* -------------------------------------------------------------------------
+ 5. Tabelas
+ ------------------------------------------------------------------------- */
+
+table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 8.5pt;
+ margin: 4mm 0 5mm;
+ break-inside: avoid;
+}
+
+caption {
+ caption-side: bottom;
+ font-size: 7.5pt;
+ color: var(--tinta-3);
+ text-align: left;
+ padding-top: 1.8mm;
+}
+
+thead {
+ break-inside: avoid;
+ break-after: avoid;
+ /* Repete o cabeçalho quando a tabela atravessa a página. */
+ display: table-header-group;
+}
+
+tbody tr { break-inside: avoid; }
+
+/* As tabelas de 12 linhas (uma por série) podem dividir: mantê-las inteiras
+ empurrava a tabela toda e deixava quase metade da página em branco. */
+table.compacta { break-inside: auto; }
+
+th {
+ font-family: "Outfit", sans-serif;
+ font-weight: 700;
+ font-size: 7.5pt;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ text-align: left;
+ color: #fff;
+ background: var(--tinta);
+ padding: 2.2mm 2.5mm;
+}
+
+td {
+ padding: 2.2mm 2.5mm;
+ border-bottom: 0.5pt solid var(--linha-suave);
+ vertical-align: top;
+}
+
+tbody tr:nth-child(even) { background: var(--fundo-suave); }
+td.destaque { font-weight: 600; color: var(--tinta); }
+td.sim { color: #0b6b52; font-weight: 600; }
+td.nao { color: var(--tinta-3); }
+
+/* -------------------------------------------------------------------------
+ 6. Código
+ ------------------------------------------------------------------------- */
+
+pre {
+ break-inside: avoid;
+ margin: 4mm 0;
+ padding: 4mm 4.5mm;
+ background: #062639;
+ border-radius: 2mm;
+ color: #e2e8f0;
+ font-family: "JetBrains Mono", ui-monospace, "Courier New", monospace;
+ font-size: 8pt;
+ line-height: 1.6;
+ white-space: pre-wrap;
+ overflow-wrap: break-word;
+}
+
+code {
+ font-family: "JetBrains Mono", ui-monospace, "Courier New", monospace;
+ font-size: 8.6pt;
+ background: #f2f6f8;
+ color: #b34700;
+ padding: 0.4mm 1.2mm;
+ border-radius: 1mm;
+}
+
+pre code { background: none; color: inherit; padding: 0; font-size: inherit; }
+
+.c-com { color: #94a3b8; font-style: italic; } /* comentário */
+.c-key { color: #c4b5fd; } /* palavra-chave */
+.c-fun { color: #7dd3fc; } /* função */
+.c-str { color: #86efac; } /* string */
+.c-num { color: #fdba74; } /* número */
+.c-pre { color: #f9a8d4; } /* diretiva */
+
+.legenda-codigo {
+ font-size: 7.5pt;
+ color: var(--tinta-3);
+ margin: -2.5mm 0 4mm;
+}
+
+/* -------------------------------------------------------------------------
+ 7. Figuras
+ ------------------------------------------------------------------------- */
+
+figure {
+ break-inside: avoid;
+ margin: 5mm 0;
+ text-align: center;
+}
+
+figure img {
+ max-width: 100%;
+ height: auto;
+ border-radius: 1.5mm;
+}
+
+/* Diagramas desenhados em SVG dentro do próprio documento, para herdarem a
+ paleta da liga. Traço fino e sem preenchimento pesado: imprimem bem. */
+.diagrama {
+ width: 100%;
+ height: auto;
+ display: block;
+}
+
+.diagrama text {
+ font-family: "Inter", system-ui, sans-serif;
+ fill: var(--tinta-2);
+}
+
+.diagrama .rotulo {
+ font-family: "Outfit", sans-serif;
+ font-weight: 700;
+ fill: var(--tinta);
+}
+
+.diagrama .miudo { fill: var(--tinta-3); }
+.diagrama .caixa { fill: #ffffff; stroke: var(--linha); stroke-width: 1.2; }
+.diagrama .caixa-destaque { fill: #fff6ea; stroke: var(--laranja); stroke-width: 1.4; }
+.diagrama .traco { stroke: var(--tinta-3); stroke-width: 1.2; fill: none; }
+.diagrama .onda { stroke: var(--prussiano); stroke-width: 1.8; fill: none; stroke-linejoin: round; }
+
+figcaption {
+ font-size: 7.5pt;
+ color: var(--tinta-3);
+ margin-top: 2mm;
+ text-align: left;
+ line-height: 1.5;
+}
+
+/* -------------------------------------------------------------------------
+ 8. Grade de séries: cada chip com a cor que o site usa
+ ------------------------------------------------------------------------- */
+
+.grade-series {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 3mm;
+ margin: 4mm 0;
+}
+
+.chip {
+ break-inside: avoid;
+ border: 0.8pt solid var(--linha);
+ border-top: 2.5pt solid var(--cor-chip, var(--laranja));
+ border-radius: 1.5mm;
+ padding: 3mm 3.5mm;
+}
+
+.chip-nome {
+ font-family: "Outfit", sans-serif;
+ font-weight: 800;
+ font-size: 11pt;
+ line-height: 1.1;
+ /* A cor da série fica na faixa superior do card, que é decoração e não
+ precisa de contraste. Dez das doze cores reprovam em AA como texto
+ (o amarelo do P4 dá 1,63:1 sobre branco), então o nome vai em tinta. */
+ color: var(--tinta);
+}
+
+.chip-papel {
+ font-size: 7.5pt;
+ font-weight: 600;
+ color: var(--tinta-3);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ margin-bottom: 2mm;
+}
+
+.chip p { font-size: 8.2pt; line-height: 1.5; margin-bottom: 2mm; text-align: left; }
+
+.chip dl {
+ margin: 0;
+ font-size: 7.6pt;
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: 0.6mm 2mm;
+ border-top: 0.5pt solid var(--linha-suave);
+ padding-top: 1.8mm;
+}
+
+.chip dt { color: var(--tinta-3); font-weight: 500; }
+.chip dd { margin: 0; color: var(--tinta); font-weight: 600; }
+
+/* -------------------------------------------------------------------------
+ 9. Sumário: numeração de página resolvida pelo Paged.js
+ ------------------------------------------------------------------------- */
+
+.sumario { margin-top: 6mm; }
+
+.sumario a {
+ display: flex;
+ align-items: baseline;
+ gap: 2mm;
+ border: none;
+ color: var(--tinta);
+ text-decoration: none;
+ padding: 1.8mm 0;
+ border-bottom: 0.5pt solid var(--linha-suave);
+ font-size: 10pt;
+}
+
+.sumario .num {
+ font-family: "Outfit", sans-serif;
+ font-weight: 700;
+ color: var(--laranja-texto);
+ min-width: 7mm;
+}
+
+.sumario .txt { font-weight: 600; }
+
+.sumario .pontos {
+ flex: 1;
+ border-bottom: 0.5pt dotted var(--linha);
+ transform: translateY(-1mm);
+}
+
+.sumario a::after {
+ content: target-counter(attr(href url), page);
+ font-family: "Outfit", sans-serif;
+ font-weight: 700;
+ color: var(--tinta-3);
+}
+
+/* -------------------------------------------------------------------------
+ 10. Passos numerados da prática
+ ------------------------------------------------------------------------- */
+
+.passo {
+ /* Indivisível: testado com break-inside:auto, o círculo com o número do
+ passo ficava sozinho no pé da página, separado do próprio conteúdo. */
+ break-inside: avoid;
+ display: grid;
+ grid-template-columns: 8mm 1fr;
+ gap: 3mm;
+ margin-bottom: 5mm;
+}
+
+.passo-num {
+ font-family: "Outfit", sans-serif;
+ font-weight: 800;
+ font-size: 12pt;
+ color: #fff;
+ background: var(--prussiano);
+ width: 8mm;
+ height: 8mm;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ line-height: 1;
+}
+
+.passo h3 { margin-top: 0.5mm; border: none; padding: 0; }
+
+/* -------------------------------------------------------------------------
+ 11. Visual só de tela (Paged.js desenha as folhas)
+ ------------------------------------------------------------------------- */
+
+@media screen {
+ body {
+ background:
+ radial-gradient(60rem 40rem at 50% -10rem, #eaf1f5 0%, transparent 70%),
+ #dfe7ec;
+ padding: 10mm 0 16mm;
+ }
+
+ /* Pilha de folhas centralizada na janela, com respiro entre elas. */
+ .pagedjs_pages {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 9mm;
+ width: 100%;
+ max-width: none;
+ }
+
+ .pagedjs_page {
+ background: #fff;
+ box-shadow:
+ 0 0 0 1px rgba(6, 38, 57, 0.07),
+ 0 6px 22px -6px rgba(6, 38, 57, 0.22);
+ border-radius: 1.5mm;
+ margin: 0;
+ transition: box-shadow 0.2s ease;
+ }
+
+ .pagedjs_page:hover {
+ box-shadow:
+ 0 0 0 1px rgba(6, 38, 57, 0.12),
+ 0 10px 30px -8px rgba(6, 38, 57, 0.3);
+ }
+
+ /* A capa é sangria total: sem cantos arredondados brigando com o desenho. */
+ .pagedjs_page:first-child { border-radius: 0; }
+
+ .aviso-carregando {
+ font-family: "Inter", sans-serif;
+ text-align: center;
+ padding: 40mm 20mm;
+ color: #5b7688;
+ font-size: 11pt;
+ }
+
+ /* Leitura confortável: um pouco mais de corpo do que no papel. */
+ .pagedjs_page_content { font-size: 10.2pt; }
+}
+
+/* Telas estreitas: encolhe a folha inteira em vez de estourar a largura. */
+@media screen and (max-width: 230mm) {
+ .pagedjs_pages {
+ transform: scale(calc(100vw / 232mm));
+ transform-origin: top center;
+ }
+}
+
+@media print {
+ .aviso-carregando { display: none; }
+}
diff --git a/public/apostilas/assets/capa-curvas.svg b/public/apostilas/assets/capa-curvas.svg
new file mode 100644
index 0000000..36cad9b
--- /dev/null
+++ b/public/apostilas/assets/capa-curvas.svg
@@ -0,0 +1,33 @@
+
diff --git a/public/apostilas/assets/laser-escuro.svg b/public/apostilas/assets/laser-escuro.svg
new file mode 100644
index 0000000..386eac9
--- /dev/null
+++ b/public/apostilas/assets/laser-escuro.svg
@@ -0,0 +1,17 @@
+
diff --git a/public/apostilas/assets/laser.svg b/public/apostilas/assets/laser.svg
new file mode 100644
index 0000000..946365c
--- /dev/null
+++ b/public/apostilas/assets/laser.svg
@@ -0,0 +1,17 @@
+
diff --git a/public/apostilas/assets/paged.polyfill.js b/public/apostilas/assets/paged.polyfill.js
new file mode 100644
index 0000000..3100799
--- /dev/null
+++ b/public/apostilas/assets/paged.polyfill.js
@@ -0,0 +1,33251 @@
+/**
+ * @license Paged.js v0.4.3 | MIT | https://gitlab.coko.foundation/pagedjs/pagedjs
+ */
+
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define(factory) :
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.PagedPolyfill = factory());
+})(this, (function () { 'use strict';
+
+ function getDefaultExportFromCjs (x) {
+ return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
+ }
+
+ var eventEmitter = {exports: {}};
+
+ var d$2 = {exports: {}};
+
+ var isImplemented$6 = function () {
+ var assign = Object.assign, obj;
+ if (typeof assign !== "function") return false;
+ obj = { foo: "raz" };
+ assign(obj, { bar: "dwa" }, { trzy: "trzy" });
+ return (obj.foo + obj.bar + obj.trzy) === "razdwatrzy";
+ };
+
+ var isImplemented$5;
+ var hasRequiredIsImplemented$1;
+
+ function requireIsImplemented$1 () {
+ if (hasRequiredIsImplemented$1) return isImplemented$5;
+ hasRequiredIsImplemented$1 = 1;
+
+ isImplemented$5 = function () {
+ try {
+ Object.keys("primitive");
+ return true;
+ } catch (e) {
+ return false;
+ }
+ };
+ return isImplemented$5;
+ }
+
+ // eslint-disable-next-line no-empty-function
+ var noop$4 = function () {};
+
+ var _undefined = noop$4(); // Support ES3 engines
+
+ var isValue$3 = function (val) {
+ return (val !== _undefined) && (val !== null);
+ };
+
+ var shim$5;
+ var hasRequiredShim$5;
+
+ function requireShim$5 () {
+ if (hasRequiredShim$5) return shim$5;
+ hasRequiredShim$5 = 1;
+
+ var isValue = isValue$3;
+
+ var keys = Object.keys;
+
+ shim$5 = function (object) {
+ return keys(isValue(object) ? Object(object) : object);
+ };
+ return shim$5;
+ }
+
+ var keys;
+ var hasRequiredKeys;
+
+ function requireKeys () {
+ if (hasRequiredKeys) return keys;
+ hasRequiredKeys = 1;
+
+ keys = requireIsImplemented$1()()
+ ? Object.keys
+ : requireShim$5();
+ return keys;
+ }
+
+ var isValue$2 = isValue$3;
+
+ var validValue = function (value) {
+ if (!isValue$2(value)) throw new TypeError("Cannot use null or undefined");
+ return value;
+ };
+
+ var shim$4;
+ var hasRequiredShim$4;
+
+ function requireShim$4 () {
+ if (hasRequiredShim$4) return shim$4;
+ hasRequiredShim$4 = 1;
+
+ var keys = requireKeys()
+ , value = validValue
+ , max = Math.max;
+
+ shim$4 = function (dest, src /*, …srcn*/) {
+ var error, i, length = max(arguments.length, 2), assign;
+ dest = Object(value(dest));
+ assign = function (key) {
+ try {
+ dest[key] = src[key];
+ } catch (e) {
+ if (!error) error = e;
+ }
+ };
+ for (i = 1; i < length; ++i) {
+ src = arguments[i];
+ keys(src).forEach(assign);
+ }
+ if (error !== undefined) throw error;
+ return dest;
+ };
+ return shim$4;
+ }
+
+ var assign$2 = isImplemented$6()
+ ? Object.assign
+ : requireShim$4();
+
+ var isValue$1 = isValue$3;
+
+ var forEach$1 = Array.prototype.forEach, create$5 = Object.create;
+
+ var process = function (src, obj) {
+ var key;
+ for (key in src) obj[key] = src[key];
+ };
+
+ // eslint-disable-next-line no-unused-vars
+ var normalizeOptions = function (opts1 /*, …options*/) {
+ var result = create$5(null);
+ forEach$1.call(arguments, function (options) {
+ if (!isValue$1(options)) return;
+ process(Object(options), result);
+ });
+ return result;
+ };
+
+ var isCallable$1 = function (obj) {
+ return typeof obj === "function";
+ };
+
+ var str = "razdwatrzy";
+
+ var isImplemented$4 = function () {
+ if (typeof str.contains !== "function") return false;
+ return (str.contains("dwa") === true) && (str.contains("foo") === false);
+ };
+
+ var shim$3;
+ var hasRequiredShim$3;
+
+ function requireShim$3 () {
+ if (hasRequiredShim$3) return shim$3;
+ hasRequiredShim$3 = 1;
+
+ var indexOf = String.prototype.indexOf;
+
+ shim$3 = function (searchString/*, position*/) {
+ return indexOf.call(this, searchString, arguments[1]) > -1;
+ };
+ return shim$3;
+ }
+
+ var contains$1 = isImplemented$4()
+ ? String.prototype.contains
+ : requireShim$3();
+
+ var assign$1 = assign$2
+ , normalizeOpts = normalizeOptions
+ , isCallable = isCallable$1
+ , contains = contains$1
+
+ , d$1;
+
+ d$1 = d$2.exports = function (dscr, value/*, options*/) {
+ var c, e, w, options, desc;
+ if ((arguments.length < 2) || (typeof dscr !== 'string')) {
+ options = value;
+ value = dscr;
+ dscr = null;
+ } else {
+ options = arguments[2];
+ }
+ if (dscr == null) {
+ c = w = true;
+ e = false;
+ } else {
+ c = contains.call(dscr, 'c');
+ e = contains.call(dscr, 'e');
+ w = contains.call(dscr, 'w');
+ }
+
+ desc = { value: value, configurable: c, enumerable: e, writable: w };
+ return !options ? desc : assign$1(normalizeOpts(options), desc);
+ };
+
+ d$1.gs = function (dscr, get, set/*, options*/) {
+ var c, e, options, desc;
+ if (typeof dscr !== 'string') {
+ options = set;
+ set = get;
+ get = dscr;
+ dscr = null;
+ } else {
+ options = arguments[3];
+ }
+ if (get == null) {
+ get = undefined;
+ } else if (!isCallable(get)) {
+ options = get;
+ get = set = undefined;
+ } else if (set == null) {
+ set = undefined;
+ } else if (!isCallable(set)) {
+ options = set;
+ set = undefined;
+ }
+ if (dscr == null) {
+ c = true;
+ e = false;
+ } else {
+ c = contains.call(dscr, 'c');
+ e = contains.call(dscr, 'e');
+ }
+
+ desc = { get: get, set: set, configurable: c, enumerable: e };
+ return !options ? desc : assign$1(normalizeOpts(options), desc);
+ };
+
+ var dExports = d$2.exports;
+
+ var validCallable = function (fn) {
+ if (typeof fn !== "function") throw new TypeError(fn + " is not a function");
+ return fn;
+ };
+
+ (function (module, exports) {
+
+ var d = dExports
+ , callable = validCallable
+
+ , apply = Function.prototype.apply, call = Function.prototype.call
+ , create = Object.create, defineProperty = Object.defineProperty
+ , defineProperties = Object.defineProperties
+ , hasOwnProperty = Object.prototype.hasOwnProperty
+ , descriptor = { configurable: true, enumerable: false, writable: true }
+
+ , on, once, off, emit, methods, descriptors, base;
+
+ on = function (type, listener) {
+ var data;
+
+ callable(listener);
+
+ if (!hasOwnProperty.call(this, '__ee__')) {
+ data = descriptor.value = create(null);
+ defineProperty(this, '__ee__', descriptor);
+ descriptor.value = null;
+ } else {
+ data = this.__ee__;
+ }
+ if (!data[type]) data[type] = listener;
+ else if (typeof data[type] === 'object') data[type].push(listener);
+ else data[type] = [data[type], listener];
+
+ return this;
+ };
+
+ once = function (type, listener) {
+ var once, self;
+
+ callable(listener);
+ self = this;
+ on.call(this, type, once = function () {
+ off.call(self, type, once);
+ apply.call(listener, this, arguments);
+ });
+
+ once.__eeOnceListener__ = listener;
+ return this;
+ };
+
+ off = function (type, listener) {
+ var data, listeners, candidate, i;
+
+ callable(listener);
+
+ if (!hasOwnProperty.call(this, '__ee__')) return this;
+ data = this.__ee__;
+ if (!data[type]) return this;
+ listeners = data[type];
+
+ if (typeof listeners === 'object') {
+ for (i = 0; (candidate = listeners[i]); ++i) {
+ if ((candidate === listener) ||
+ (candidate.__eeOnceListener__ === listener)) {
+ if (listeners.length === 2) data[type] = listeners[i ? 0 : 1];
+ else listeners.splice(i, 1);
+ }
+ }
+ } else {
+ if ((listeners === listener) ||
+ (listeners.__eeOnceListener__ === listener)) {
+ delete data[type];
+ }
+ }
+
+ return this;
+ };
+
+ emit = function (type) {
+ var i, l, listener, listeners, args;
+
+ if (!hasOwnProperty.call(this, '__ee__')) return;
+ listeners = this.__ee__[type];
+ if (!listeners) return;
+
+ if (typeof listeners === 'object') {
+ l = arguments.length;
+ args = new Array(l - 1);
+ for (i = 1; i < l; ++i) args[i - 1] = arguments[i];
+
+ listeners = listeners.slice();
+ for (i = 0; (listener = listeners[i]); ++i) {
+ apply.call(listener, this, args);
+ }
+ } else {
+ switch (arguments.length) {
+ case 1:
+ call.call(listeners, this);
+ break;
+ case 2:
+ call.call(listeners, this, arguments[1]);
+ break;
+ case 3:
+ call.call(listeners, this, arguments[1], arguments[2]);
+ break;
+ default:
+ l = arguments.length;
+ args = new Array(l - 1);
+ for (i = 1; i < l; ++i) {
+ args[i - 1] = arguments[i];
+ }
+ apply.call(listeners, this, args);
+ }
+ }
+ };
+
+ methods = {
+ on: on,
+ once: once,
+ off: off,
+ emit: emit
+ };
+
+ descriptors = {
+ on: d(on),
+ once: d(once),
+ off: d(off),
+ emit: d(emit)
+ };
+
+ base = defineProperties({}, descriptors);
+
+ module.exports = exports = function (o) {
+ return (o == null) ? create(base) : defineProperties(Object(o), descriptors);
+ };
+ exports.methods = methods;
+ } (eventEmitter, eventEmitter.exports));
+
+ var eventEmitterExports = eventEmitter.exports;
+ var EventEmitter = /*@__PURE__*/getDefaultExportFromCjs(eventEmitterExports);
+
+ /**
+ * Hooks allow for injecting functions that must all complete in order before finishing
+ * They will execute in parallel but all must finish before continuing
+ * Functions may return a promise if they are asycn.
+ * From epubjs/src/utils/hooks
+ * @param {any} context scope of this
+ * @example this.content = new Hook(this);
+ */
+ class Hook {
+ constructor(context){
+ this.context = context || this;
+ this.hooks = [];
+ }
+
+ /**
+ * Adds a function to be run before a hook completes
+ * @example this.content.register(function(){...});
+ * @return {undefined} void
+ */
+ register(){
+ for(var i = 0; i < arguments.length; ++i) {
+ if (typeof arguments[i] === "function") {
+ this.hooks.push(arguments[i]);
+ } else {
+ // unpack array
+ for(var j = 0; j < arguments[i].length; ++j) {
+ this.hooks.push(arguments[i][j]);
+ }
+ }
+ }
+ }
+
+ /**
+ * Triggers a hook to run all functions
+ * @example this.content.trigger(args).then(function(){...});
+ * @return {Promise} results
+ */
+ trigger(){
+ var args = arguments;
+ var context = this.context;
+ var promises = [];
+
+ this.hooks.forEach(function(task) {
+ var executing = task.apply(context, args);
+
+ if(executing && typeof executing["then"] === "function") {
+ // Task is a function that returns a promise
+ promises.push(executing);
+ } else {
+ // Otherwise Task resolves immediately, add resolved promise with result
+ promises.push(new Promise((resolve, reject) => {
+ resolve(executing);
+ }));
+ }
+ });
+
+
+ return Promise.all(promises);
+ }
+
+ /**
+ * Triggers a hook to run all functions synchronously
+ * @example this.content.trigger(args).then(function(){...});
+ * @return {Array} results
+ */
+ triggerSync(){
+ var args = arguments;
+ var context = this.context;
+ var results = [];
+
+ this.hooks.forEach(function(task) {
+ var executing = task.apply(context, args);
+
+ results.push(executing);
+ });
+
+
+ return results;
+ }
+
+ // Adds a function to be run before a hook completes
+ list(){
+ return this.hooks;
+ }
+
+ clear(){
+ return this.hooks = [];
+ }
+ }
+
+ function getBoundingClientRect(element) {
+ if (!element) {
+ return;
+ }
+ let rect;
+ if (typeof element.getBoundingClientRect !== "undefined") {
+ rect = element.getBoundingClientRect();
+ } else {
+ let range = document.createRange();
+ range.selectNode(element);
+ rect = range.getBoundingClientRect();
+ }
+ return rect;
+ }
+
+ function getClientRects(element) {
+ if (!element) {
+ return;
+ }
+ let rect;
+ if (typeof element.getClientRects !== "undefined") {
+ rect = element.getClientRects();
+ } else {
+ let range = document.createRange();
+ range.selectNode(element);
+ rect = range.getClientRects();
+ }
+ return rect;
+ }
+
+ /**
+ * Generates a UUID
+ * based on: http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
+ * @returns {string} uuid
+ */
+ function UUID() {
+ var d = new Date().getTime();
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
+ d += performance.now(); //use high-precision timer if available
+ }
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
+ var r = (d + Math.random() * 16) % 16 | 0;
+ d = Math.floor(d / 16);
+ return (c === "x" ? r : (r & 0x3 | 0x8)).toString(16);
+ });
+ }
+
+ function attr(element, attributes) {
+ for (var i = 0; i < attributes.length; i++) {
+ if (element.hasAttribute(attributes[i])) {
+ return element.getAttribute(attributes[i]);
+ }
+ }
+ }
+
+ /* Based on by https://mths.be/cssescape v1.5.1 by @mathias | MIT license
+ * Allows # and .
+ */
+ function querySelectorEscape(value) {
+ if (arguments.length == 0) {
+ throw new TypeError("`CSS.escape` requires an argument.");
+ }
+ var string = String(value);
+
+ var length = string.length;
+ var index = -1;
+ var codeUnit;
+ var result = "";
+ var firstCodeUnit = string.charCodeAt(0);
+ while (++index < length) {
+ codeUnit = string.charCodeAt(index);
+
+
+
+ // Note: there’s no need to special-case astral symbols, surrogate
+ // pairs, or lone surrogates.
+
+ // If the character is NULL (U+0000), then the REPLACEMENT CHARACTER
+ // (U+FFFD).
+ if (codeUnit == 0x0000) {
+ result += "\uFFFD";
+ continue;
+ }
+
+ if (
+ // If the character is in the range [\1-\1F] (U+0001 to U+001F) or is
+ // U+007F, […]
+ (codeUnit >= 0x0001 && codeUnit <= 0x001F) || codeUnit == 0x007F ||
+ // If the character is the first character and is in the range [0-9]
+ // (U+0030 to U+0039), […]
+ (index == 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) ||
+ // If the character is the second character and is in the range [0-9]
+ // (U+0030 to U+0039) and the first character is a `-` (U+002D), […]
+ (
+ index == 1 &&
+ codeUnit >= 0x0030 && codeUnit <= 0x0039 &&
+ firstCodeUnit == 0x002D
+ )
+ ) {
+ // https://drafts.csswg.org/cssom/#escape-a-character-as-code-point
+ result += "\\" + codeUnit.toString(16) + " ";
+ continue;
+ }
+
+ if (
+ // If the character is the first character and is a `-` (U+002D), and
+ // there is no second character, […]
+ index == 0 &&
+ length == 1 &&
+ codeUnit == 0x002D
+ ) {
+ result += "\\" + string.charAt(index);
+ continue;
+ }
+
+ // support for period character in id
+ if (codeUnit == 0x002E) {
+ if (string.charAt(0) == "#") {
+ result += "\\.";
+ continue;
+ }
+ }
+
+
+ // If the character is not handled by one of the above rules and is
+ // greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or
+ // is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to
+ // U+005A), or [a-z] (U+0061 to U+007A), […]
+ if (
+ codeUnit >= 0x0080 ||
+ codeUnit == 0x002D ||
+ codeUnit == 0x005F ||
+ codeUnit == 35 || // Allow #
+ codeUnit == 46 || // Allow .
+ codeUnit >= 0x0030 && codeUnit <= 0x0039 ||
+ codeUnit >= 0x0041 && codeUnit <= 0x005A ||
+ codeUnit >= 0x0061 && codeUnit <= 0x007A
+ ) {
+ // the character itself
+ result += string.charAt(index);
+ continue;
+ }
+
+ // Otherwise, the escaped character.
+ // https://drafts.csswg.org/cssom/#escape-a-character
+ result += "\\" + string.charAt(index);
+
+ }
+ return result;
+ }
+
+ /**
+ * Creates a new pending promise and provides methods to resolve or reject it.
+ * From: https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Promise.jsm/Deferred#backwards_forwards_compatible
+ * @returns {object} defered
+ */
+ function defer() {
+ this.resolve = null;
+
+ this.reject = null;
+
+ this.id = UUID();
+
+ this.promise = new Promise((resolve, reject) => {
+ this.resolve = resolve;
+ this.reject = reject;
+ });
+ Object.freeze(this);
+ }
+
+ const requestIdleCallback = typeof window !== "undefined" && ("requestIdleCallback" in window ? window.requestIdleCallback : window.requestAnimationFrame);
+
+ function CSSValueToString(obj) {
+ return obj.value + (obj.unit || "");
+ }
+
+ function isElement(node) {
+ return node && node.nodeType === 1;
+ }
+
+ function isText(node) {
+ return node && node.nodeType === 3;
+ }
+
+ function* walk$2(start, limiter) {
+ let node = start;
+
+ while (node) {
+
+ yield node;
+
+ if (node.childNodes.length) {
+ node = node.firstChild;
+ } else if (node.nextSibling) {
+ if (limiter && node === limiter) {
+ node = undefined;
+ break;
+ }
+ node = node.nextSibling;
+ } else {
+ while (node) {
+ node = node.parentNode;
+ if (limiter && node === limiter) {
+ node = undefined;
+ break;
+ }
+ if (node && node.nextSibling) {
+ node = node.nextSibling;
+ break;
+ }
+
+ }
+ }
+ }
+ }
+
+ function nodeAfter(node, limiter) {
+ if (limiter && node === limiter) {
+ return;
+ }
+ let significantNode = nextSignificantNode(node);
+ if (significantNode) {
+ return significantNode;
+ }
+ if (node.parentNode) {
+ while ((node = node.parentNode)) {
+ if (limiter && node === limiter) {
+ return;
+ }
+ significantNode = nextSignificantNode(node);
+ if (significantNode) {
+ return significantNode;
+ }
+ }
+ }
+ }
+
+ function nodeBefore(node, limiter) {
+ if (limiter && node === limiter) {
+ return;
+ }
+ let significantNode = previousSignificantNode(node);
+ if (significantNode) {
+ return significantNode;
+ }
+ if (node.parentNode) {
+ while ((node = node.parentNode)) {
+ if (limiter && node === limiter) {
+ return;
+ }
+ significantNode = previousSignificantNode(node);
+ if (significantNode) {
+ return significantNode;
+ }
+ }
+ }
+ }
+
+ function elementAfter(node, limiter) {
+ let after = nodeAfter(node, limiter);
+
+ while (after && after.nodeType !== 1) {
+ after = nodeAfter(after, limiter);
+ }
+
+ return after;
+ }
+
+ function elementBefore(node, limiter) {
+ let before = nodeBefore(node, limiter);
+
+ while (before && before.nodeType !== 1) {
+ before = nodeBefore(before, limiter);
+ }
+
+ return before;
+ }
+
+ function displayedElementAfter(node, limiter) {
+ let after = elementAfter(node, limiter);
+
+ while (after && after.dataset.undisplayed) {
+ after = elementAfter(after, limiter);
+ }
+
+ return after;
+ }
+
+ function displayedElementBefore(node, limiter) {
+ let before = elementBefore(node, limiter);
+
+ while (before && before.dataset.undisplayed) {
+ before = elementBefore(before, limiter);
+ }
+
+ return before;
+ }
+
+ function rebuildAncestors(node) {
+ let parent, ancestor;
+ let ancestors = [];
+ let added = [];
+
+ let fragment = document.createDocumentFragment();
+
+ // Handle rowspan on table
+ if (node.nodeName === "TR") {
+ let previousRow = node.previousElementSibling;
+ let previousRowDistance = 1;
+ while (previousRow) {
+ // previous row has more columns, might indicate a rowspan.
+ if (previousRow.childElementCount > node.childElementCount) {
+ const initialColumns = Array.from(node.children);
+ while (node.firstChild) {
+ node.firstChild.remove();
+ }
+ let k = 0;
+ for (let j = 0; j < previousRow.children.length; j++) {
+ let column = previousRow.children[j];
+ if (column.rowSpan && column.rowSpan > previousRowDistance) {
+ const duplicatedColumn = column.cloneNode(true);
+ // Adjust rowspan value
+ duplicatedColumn.rowSpan = column.rowSpan - previousRowDistance;
+ // Add the column to the row
+ node.appendChild(duplicatedColumn);
+ } else {
+ // Fill the gap with the initial columns (if exists)
+ const initialColumn = initialColumns[k++];
+ // The initial column can be undefined if the newly created table has less columns than the original table
+ if (initialColumn) {
+ node.appendChild(initialColumn);
+ }
+ }
+ }
+ }
+ previousRow = previousRow.previousElementSibling;
+ previousRowDistance++;
+ }
+ }
+
+ // Gather all ancestors
+ let element = node;
+ while(element.parentNode && element.parentNode.nodeType === 1) {
+ ancestors.unshift(element.parentNode);
+ element = element.parentNode;
+ }
+
+ for (var i = 0; i < ancestors.length; i++) {
+ ancestor = ancestors[i];
+ parent = ancestor.cloneNode(false);
+
+ parent.setAttribute("data-split-from", parent.getAttribute("data-ref"));
+ // ancestor.setAttribute("data-split-to", parent.getAttribute("data-ref"));
+
+ if (parent.hasAttribute("id")) {
+ let dataID = parent.getAttribute("id");
+ parent.setAttribute("data-id", dataID);
+ parent.removeAttribute("id");
+ }
+
+ // This is handled by css :not, but also tidied up here
+ if (parent.hasAttribute("data-break-before")) {
+ parent.removeAttribute("data-break-before");
+ }
+
+ if (parent.hasAttribute("data-previous-break-after")) {
+ parent.removeAttribute("data-previous-break-after");
+ }
+
+ if (added.length) {
+ let container = added[added.length-1];
+ container.appendChild(parent);
+ } else {
+ fragment.appendChild(parent);
+ }
+ added.push(parent);
+
+ // rebuild table rows
+ if (parent.nodeName === "TD" && ancestor.parentElement.contains(ancestor)) {
+ let td = ancestor;
+ let prev = parent;
+ while ((td = td.previousElementSibling)) {
+ let sib = td.cloneNode(false);
+ parent.parentElement.insertBefore(sib, prev);
+ prev = sib;
+ }
+
+ }
+ }
+
+ added = undefined;
+ return fragment;
+ }
+ /*
+ export function split(bound, cutElement, breakAfter) {
+ let needsRemoval = [];
+ let index = indexOf(cutElement);
+
+ if (!breakAfter && index === 0) {
+ return;
+ }
+
+ if (breakAfter && index === (cutElement.parentNode.children.length - 1)) {
+ return;
+ }
+
+ // Create a fragment with rebuilt ancestors
+ let fragment = rebuildAncestors(cutElement);
+
+ // Clone cut
+ if (!breakAfter) {
+ let clone = cutElement.cloneNode(true);
+ let ref = cutElement.parentNode.getAttribute('data-ref');
+ let parent = fragment.querySelector("[data-ref='" + ref + "']");
+ parent.appendChild(clone);
+ needsRemoval.push(cutElement);
+ }
+
+ // Remove all after cut
+ let next = nodeAfter(cutElement, bound);
+ while (next) {
+ let clone = next.cloneNode(true);
+ let ref = next.parentNode.getAttribute('data-ref');
+ let parent = fragment.querySelector("[data-ref='" + ref + "']");
+ parent.appendChild(clone);
+ needsRemoval.push(next);
+ next = nodeAfter(next, bound);
+ }
+
+ // Remove originals
+ needsRemoval.forEach((node) => {
+ if (node) {
+ node.remove();
+ }
+ });
+
+ // Insert after bounds
+ bound.parentNode.insertBefore(fragment, bound.nextSibling);
+ return [bound, bound.nextSibling];
+ }
+ */
+
+ function needsBreakBefore(node) {
+ if( typeof node !== "undefined" &&
+ typeof node.dataset !== "undefined" &&
+ typeof node.dataset.breakBefore !== "undefined" &&
+ (node.dataset.breakBefore === "always" ||
+ node.dataset.breakBefore === "page" ||
+ node.dataset.breakBefore === "left" ||
+ node.dataset.breakBefore === "right" ||
+ node.dataset.breakBefore === "recto" ||
+ node.dataset.breakBefore === "verso")
+ ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ function needsPreviousBreakAfter(node) {
+ if( typeof node !== "undefined" &&
+ typeof node.dataset !== "undefined" &&
+ typeof node.dataset.previousBreakAfter !== "undefined" &&
+ (node.dataset.previousBreakAfter === "always" ||
+ node.dataset.previousBreakAfter === "page" ||
+ node.dataset.previousBreakAfter === "left" ||
+ node.dataset.previousBreakAfter === "right" ||
+ node.dataset.previousBreakAfter === "recto" ||
+ node.dataset.previousBreakAfter === "verso")
+ ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ function needsPageBreak(node, previousSignificantNode) {
+ if (typeof node === "undefined" || !previousSignificantNode || isIgnorable(node)) {
+ return false;
+ }
+ if (node.dataset && node.dataset.undisplayed) {
+ return false;
+ }
+ let previousSignificantNodePage = previousSignificantNode.dataset ? previousSignificantNode.dataset.page : undefined;
+ if (typeof previousSignificantNodePage === "undefined") {
+ const nodeWithNamedPage = getNodeWithNamedPage(previousSignificantNode);
+ if (nodeWithNamedPage) {
+ previousSignificantNodePage = nodeWithNamedPage.dataset.page;
+ }
+ }
+ let currentNodePage = node.dataset ? node.dataset.page : undefined;
+ if (typeof currentNodePage === "undefined") {
+ const nodeWithNamedPage = getNodeWithNamedPage(node, previousSignificantNode);
+ if (nodeWithNamedPage) {
+ currentNodePage = nodeWithNamedPage.dataset.page;
+ }
+ }
+ return currentNodePage !== previousSignificantNodePage;
+ }
+
+ function *words(node) {
+ let currentText = node.nodeValue;
+ let max = currentText.length;
+ let currentOffset = 0;
+ let currentLetter;
+
+ let range;
+ const significantWhitespaces = node.parentElement && node.parentElement.nodeName === "PRE";
+
+ while (currentOffset < max) {
+ currentLetter = currentText[currentOffset];
+ if (/^[\S\u202F\u00A0]$/.test(currentLetter) || significantWhitespaces) {
+ if (!range) {
+ range = document.createRange();
+ range.setStart(node, currentOffset);
+ }
+ } else {
+ if (range) {
+ range.setEnd(node, currentOffset);
+ yield range;
+ range = undefined;
+ }
+ }
+
+ currentOffset += 1;
+ }
+
+ if (range) {
+ range.setEnd(node, currentOffset);
+ yield range;
+ }
+ }
+
+ function *letters(wordRange) {
+ let currentText = wordRange.startContainer;
+ let max = currentText.length;
+ let currentOffset = wordRange.startOffset;
+ // let currentLetter;
+
+ let range;
+
+ while(currentOffset < max) {
+ // currentLetter = currentText[currentOffset];
+ range = document.createRange();
+ range.setStart(currentText, currentOffset);
+ range.setEnd(currentText, currentOffset+1);
+
+ yield range;
+
+ currentOffset += 1;
+ }
+ }
+
+ function isContainer(node) {
+ let container;
+
+ if (typeof node.tagName === "undefined") {
+ return true;
+ }
+
+ if (node.style && node.style.display === "none") {
+ return false;
+ }
+
+ switch (node.tagName) {
+ // Inline
+ case "A":
+ case "ABBR":
+ case "ACRONYM":
+ case "B":
+ case "BDO":
+ case "BIG":
+ case "BR":
+ case "BUTTON":
+ case "CITE":
+ case "CODE":
+ case "DFN":
+ case "EM":
+ case "I":
+ case "IMG":
+ case "INPUT":
+ case "KBD":
+ case "LABEL":
+ case "MAP":
+ case "OBJECT":
+ case "Q":
+ case "SAMP":
+ case "SCRIPT":
+ case "SELECT":
+ case "SMALL":
+ case "SPAN":
+ case "STRONG":
+ case "SUB":
+ case "SUP":
+ case "TEXTAREA":
+ case "TIME":
+ case "TT":
+ case "VAR":
+ case "P":
+ case "H1":
+ case "H2":
+ case "H3":
+ case "H4":
+ case "H5":
+ case "H6":
+ case "FIGCAPTION":
+ case "BLOCKQUOTE":
+ case "PRE":
+ case "LI":
+ case "TD":
+ case "DT":
+ case "DD":
+ case "VIDEO":
+ case "CANVAS":
+ container = false;
+ break;
+ default:
+ container = true;
+ }
+
+ return container;
+ }
+
+ function cloneNode(n, deep=false) {
+ return n.cloneNode(deep);
+ }
+
+ function findElement(node, doc, forceQuery) {
+ const ref = node.getAttribute("data-ref");
+ return findRef(ref, doc, forceQuery);
+ }
+
+ function findRef(ref, doc, forceQuery) {
+ if (!forceQuery && doc.indexOfRefs && doc.indexOfRefs[ref]) {
+ return doc.indexOfRefs[ref];
+ } else {
+ return doc.querySelector(`[data-ref='${ref}']`);
+ }
+ }
+
+ function validNode(node) {
+ if (isText(node)) {
+ return true;
+ }
+
+ if (isElement(node) && node.dataset.ref) {
+ return true;
+ }
+
+ return false;
+ }
+
+ function prevValidNode(node) {
+ while (!validNode(node)) {
+ if (node.previousSibling) {
+ node = node.previousSibling;
+ } else {
+ node = node.parentNode;
+ }
+
+ if (!node) {
+ break;
+ }
+ }
+
+ return node;
+ }
+
+
+ function indexOf$2(node) {
+ let parent = node.parentNode;
+ if (!parent) {
+ return 0;
+ }
+ return Array.prototype.indexOf.call(parent.childNodes, node);
+ }
+
+ function child(node, index) {
+ return node.childNodes[index];
+ }
+
+ function hasContent(node) {
+ if (isElement(node)) {
+ return true;
+ } else if (isText(node) &&
+ node.textContent.trim().length) {
+ return true;
+ }
+ return false;
+ }
+
+ function indexOfTextNode(node, parent) {
+ if (!isText(node)) {
+ return -1;
+ }
+ let nodeTextContent = node.textContent;
+ let child;
+ let index = -1;
+ for (var i = 0; i < parent.childNodes.length; i++) {
+ child = parent.childNodes[i];
+ if (child.nodeType === 3) {
+ let text = parent.childNodes[i].textContent;
+ if (text.includes(nodeTextContent)) {
+ index = i;
+ break;
+ }
+ }
+ }
+
+ return index;
+ }
+
+
+ /**
+ * Throughout, whitespace is defined as one of the characters
+ * "\t" TAB \u0009
+ * "\n" LF \u000A
+ * "\r" CR \u000D
+ * " " SPC \u0020
+ *
+ * This does not use Javascript's "\s" because that includes non-breaking
+ * spaces (and also some other characters).
+ */
+
+ /**
+ * Determine if a node should be ignored by the iterator functions.
+ * taken from https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace#Whitespace_helper_functions
+ *
+ * @param {Node} node An object implementing the DOM1 |Node| interface.
+ * @return {boolean} true if the node is:
+ * 1) A |Text| node that is all whitespace
+ * 2) A |Comment| node
+ * and otherwise false.
+ */
+ function isIgnorable(node) {
+ return (node.nodeType === 8) || // A comment node
+ ((node.nodeType === 3) && isAllWhitespace(node)); // a text node, all whitespace
+ }
+
+ /**
+ * Determine whether a node's text content is entirely whitespace.
+ *
+ * @param {Node} node A node implementing the |CharacterData| interface (i.e., a |Text|, |Comment|, or |CDATASection| node
+ * @return {boolean} true if all of the text content of |nod| is whitespace, otherwise false.
+ */
+ function isAllWhitespace(node) {
+ return !(/[^\t\n\r ]/.test(node.textContent));
+ }
+
+ /**
+ * Version of |previousSibling| that skips nodes that are entirely
+ * whitespace or comments. (Normally |previousSibling| is a property
+ * of all DOM nodes that gives the sibling node, the node that is
+ * a child of the same parent, that occurs immediately before the
+ * reference node.)
+ *
+ * @param {ChildNode} sib The reference node.
+ * @return {Node|null} Either:
+ * 1) The closest previous sibling to |sib| that is not ignorable according to |is_ignorable|, or
+ * 2) null if no such node exists.
+ */
+ function previousSignificantNode(sib) {
+ while ((sib = sib.previousSibling)) {
+ if (!isIgnorable(sib)) return sib;
+ }
+ return null;
+ }
+
+ function getNodeWithNamedPage(node, limiter) {
+ if (node && node.dataset && node.dataset.page) {
+ return node;
+ }
+ if (node.parentNode) {
+ while ((node = node.parentNode)) {
+ if (limiter && node === limiter) {
+ return;
+ }
+ if (node.dataset && node.dataset.page) {
+ return node;
+ }
+ }
+ }
+ return null;
+ }
+
+ function breakInsideAvoidParentNode(node) {
+ while ((node = node.parentNode)) {
+ if (node && node.dataset && node.dataset.breakInside === "avoid") {
+ return node;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Find a parent with a given node name.
+ * @param {Node} node - initial Node
+ * @param {string} nodeName - node name (eg. "TD", "TABLE", "STRONG"...)
+ * @param {Node} limiter - go up to the parent until there's no more parent or the current node is equals to the limiter
+ * @returns {Node|undefined} - Either:
+ * 1) The closest parent for a the given node name, or
+ * 2) undefined if no such node exists.
+ */
+ function parentOf(node, nodeName, limiter) {
+ if (limiter && node === limiter) {
+ return;
+ }
+ if (node.parentNode) {
+ while ((node = node.parentNode)) {
+ if (limiter && node === limiter) {
+ return;
+ }
+ if (node.nodeName === nodeName) {
+ return node;
+ }
+ }
+ }
+ }
+
+ /**
+ * Version of |nextSibling| that skips nodes that are entirely
+ * whitespace or comments.
+ *
+ * @param {ChildNode} sib The reference node.
+ * @return {Node|null} Either:
+ * 1) The closest next sibling to |sib| that is not ignorable according to |is_ignorable|, or
+ * 2) null if no such node exists.
+ */
+ function nextSignificantNode(sib) {
+ while ((sib = sib.nextSibling)) {
+ if (!isIgnorable(sib)) return sib;
+ }
+ return null;
+ }
+
+ function filterTree(content, func, what) {
+ const treeWalker = document.createTreeWalker(
+ content || this.dom,
+ what || NodeFilter.SHOW_ALL,
+ func ? { acceptNode: func } : null,
+ false
+ );
+
+ let node;
+ let current;
+ node = treeWalker.nextNode();
+ while(node) {
+ current = node;
+ node = treeWalker.nextNode();
+ current.parentNode.removeChild(current);
+ }
+ }
+
+ /**
+ * BreakToken
+ * @class
+ */
+ class BreakToken {
+
+ constructor(node, offset) {
+ this.node = node;
+ this.offset = offset;
+ }
+
+ equals(otherBreakToken) {
+ if (!otherBreakToken) {
+ return false;
+ }
+ if (this["node"] && otherBreakToken["node"] &&
+ this["node"] !== otherBreakToken["node"]) {
+ return false;
+ }
+ if (this["offset"] && otherBreakToken["offset"] &&
+ this["offset"] !== otherBreakToken["offset"]) {
+ return false;
+ }
+ return true;
+ }
+
+ toJSON(hash) {
+ let node;
+ let index = 0;
+ if (!this.node) {
+ return {};
+ }
+ if (isElement(this.node) && this.node.dataset.ref) {
+ node = this.node.dataset.ref;
+ } else if (hash) {
+ node = this.node.parentElement.dataset.ref;
+ }
+
+ if (this.node.parentElement) {
+ const children = Array.from(this.node.parentElement.childNodes);
+ index = children.indexOf(this.node);
+ }
+
+ return JSON.stringify({
+ "node": node,
+ "index" : index,
+ "offset": this.offset
+ });
+ }
+
+ }
+
+ /**
+ * Render result.
+ * @class
+ */
+ class RenderResult {
+
+ constructor(breakToken, error) {
+ this.breakToken = breakToken;
+ this.error = error;
+ }
+ }
+
+ class OverflowContentError extends Error {
+ constructor(message, items) {
+ super(message);
+ this.items = items;
+ }
+ }
+
+ const MAX_CHARS_PER_BREAK = 1500;
+
+ /**
+ * Layout
+ * @class
+ */
+ class Layout {
+
+ constructor(element, hooks, options) {
+ this.element = element;
+
+ this.bounds = this.element.getBoundingClientRect();
+ this.parentBounds = this.element.offsetParent.getBoundingClientRect();
+ let gap = parseFloat(window.getComputedStyle(this.element).columnGap);
+
+ if (gap) {
+ let leftMargin = this.bounds.left - this.parentBounds.left;
+ this.gap = gap - leftMargin;
+ } else {
+ this.gap = 0;
+ }
+
+ if (hooks) {
+ this.hooks = hooks;
+ } else {
+ this.hooks = {};
+ this.hooks.onPageLayout = new Hook();
+ this.hooks.layout = new Hook();
+ this.hooks.renderNode = new Hook();
+ this.hooks.layoutNode = new Hook();
+ this.hooks.beforeOverflow = new Hook();
+ this.hooks.onOverflow = new Hook();
+ this.hooks.afterOverflowRemoved = new Hook();
+ this.hooks.onBreakToken = new Hook();
+ this.hooks.beforeRenderResult = new Hook();
+ }
+
+ this.settings = options || {};
+
+ this.maxChars = this.settings.maxChars || MAX_CHARS_PER_BREAK;
+ this.forceRenderBreak = false;
+ }
+
+ async renderTo(wrapper, source, breakToken, bounds = this.bounds) {
+ let start = this.getStart(source, breakToken);
+ let walker = walk$2(start, source);
+
+ let node;
+ let prevNode;
+ let done;
+ let next;
+
+ let hasRenderedContent = false;
+ let newBreakToken;
+
+ let length = 0;
+
+ let prevBreakToken = breakToken || new BreakToken(start);
+
+ this.hooks && this.hooks.onPageLayout.trigger(wrapper, prevBreakToken, this);
+
+ while (!done && !newBreakToken) {
+ next = walker.next();
+ prevNode = node;
+ node = next.value;
+ done = next.done;
+
+ if (!node) {
+ this.hooks && this.hooks.layout.trigger(wrapper, this);
+
+ let imgs = wrapper.querySelectorAll("img");
+ if (imgs.length) {
+ await this.waitForImages(imgs);
+ }
+
+ newBreakToken = this.findBreakToken(wrapper, source, bounds, prevBreakToken);
+
+ if (newBreakToken && newBreakToken.equals(prevBreakToken)) {
+ console.warn("Unable to layout item: ", prevNode);
+ this.hooks && this.hooks.beforeRenderResult.trigger(undefined, wrapper, this);
+ return new RenderResult(undefined, new OverflowContentError("Unable to layout item", [prevNode]));
+ }
+
+ this.rebuildTableFromBreakToken(newBreakToken, wrapper);
+
+ this.hooks && this.hooks.beforeRenderResult.trigger(newBreakToken, wrapper, this);
+ return new RenderResult(newBreakToken);
+ }
+
+ this.hooks && this.hooks.layoutNode.trigger(node);
+
+ // Check if the rendered element has a break set
+ if (hasRenderedContent && this.shouldBreak(node, start)) {
+ this.hooks && this.hooks.layout.trigger(wrapper, this);
+
+ let imgs = wrapper.querySelectorAll("img");
+ if (imgs.length) {
+ await this.waitForImages(imgs);
+ }
+
+ newBreakToken = this.findBreakToken(wrapper, source, bounds, prevBreakToken);
+
+ if (!newBreakToken) {
+ newBreakToken = this.breakAt(node);
+ } else {
+ this.rebuildTableFromBreakToken(newBreakToken, wrapper);
+ }
+
+ if (newBreakToken && newBreakToken.equals(prevBreakToken)) {
+ console.warn("Unable to layout item: ", node);
+ let after = newBreakToken.node && nodeAfter(newBreakToken.node);
+ if (after) {
+ newBreakToken = new BreakToken(after);
+ } else {
+ return new RenderResult(undefined, new OverflowContentError("Unable to layout item", [node]));
+ }
+ }
+
+ length = 0;
+
+ break;
+ }
+
+ if (node.dataset && node.dataset.page) {
+ let named = node.dataset.page;
+ let page = this.element.closest(".pagedjs_page");
+ page.classList.add("pagedjs_named_page");
+ page.classList.add("pagedjs_" + named + "_page");
+
+ if (!node.dataset.splitFrom) {
+ page.classList.add("pagedjs_" + named + "_first_page");
+ }
+ }
+
+ // Should the Node be a shallow or deep clone
+ let shallow = isContainer(node);
+
+ let rendered = this.append(node, wrapper, breakToken, shallow);
+
+ length += rendered.textContent.length;
+
+ // Check if layout has content yet
+ if (!hasRenderedContent) {
+ hasRenderedContent = hasContent(node);
+ }
+
+ // Skip to the next node if a deep clone was rendered
+ if (!shallow) {
+ walker = walk$2(nodeAfter(node, source), source);
+ }
+
+ if (this.forceRenderBreak) {
+ this.hooks && this.hooks.layout.trigger(wrapper, this);
+
+ newBreakToken = this.findBreakToken(wrapper, source, bounds, prevBreakToken);
+
+ if (!newBreakToken) {
+ newBreakToken = this.breakAt(node);
+ } else {
+ this.rebuildTableFromBreakToken(newBreakToken, wrapper);
+ }
+
+ length = 0;
+ this.forceRenderBreak = false;
+
+ break;
+ }
+
+ // Only check x characters
+ if (length >= this.maxChars) {
+
+ this.hooks && this.hooks.layout.trigger(wrapper, this);
+
+ let imgs = wrapper.querySelectorAll("img");
+ if (imgs.length) {
+ await this.waitForImages(imgs);
+ }
+
+ newBreakToken = this.findBreakToken(wrapper, source, bounds, prevBreakToken);
+
+ if (newBreakToken) {
+ length = 0;
+ this.rebuildTableFromBreakToken(newBreakToken, wrapper);
+ }
+
+ if (newBreakToken && newBreakToken.equals(prevBreakToken)) {
+ console.warn("Unable to layout item: ", node);
+ let after = newBreakToken.node && nodeAfter(newBreakToken.node);
+ if (after) {
+ newBreakToken = new BreakToken(after);
+ } else {
+ this.hooks && this.hooks.beforeRenderResult.trigger(undefined, wrapper, this);
+ return new RenderResult(undefined, new OverflowContentError("Unable to layout item", [node]));
+ }
+ }
+ }
+
+ }
+
+ this.hooks && this.hooks.beforeRenderResult.trigger(newBreakToken, wrapper, this);
+ return new RenderResult(newBreakToken);
+ }
+
+ breakAt(node, offset = 0) {
+ let newBreakToken = new BreakToken(
+ node,
+ offset
+ );
+ let breakHooks = this.hooks.onBreakToken.triggerSync(newBreakToken, undefined, node, this);
+ breakHooks.forEach((newToken) => {
+ if (typeof newToken != "undefined") {
+ newBreakToken = newToken;
+ }
+ });
+
+ return newBreakToken;
+ }
+
+ shouldBreak(node, limiter) {
+ let previousNode = nodeBefore(node, limiter);
+ let parentNode = node.parentNode;
+ let parentBreakBefore = needsBreakBefore(node) && parentNode && !previousNode && needsBreakBefore(parentNode);
+ let doubleBreakBefore;
+
+ if (parentBreakBefore) {
+ doubleBreakBefore = node.dataset.breakBefore === parentNode.dataset.breakBefore;
+ }
+
+ return !doubleBreakBefore && needsBreakBefore(node) || needsPreviousBreakAfter(node) || needsPageBreak(node, previousNode);
+ }
+
+ forceBreak() {
+ this.forceRenderBreak = true;
+ }
+
+ getStart(source, breakToken) {
+ let start;
+ let node = breakToken && breakToken.node;
+
+ if (node) {
+ start = node;
+ } else {
+ start = source.firstChild;
+ }
+
+ return start;
+ }
+
+ append(node, dest, breakToken, shallow = true, rebuild = true) {
+
+ let clone = cloneNode(node, !shallow);
+
+ if (node.parentNode && isElement(node.parentNode)) {
+ let parent = findElement(node.parentNode, dest);
+ // Rebuild chain
+ if (parent) {
+ parent.appendChild(clone);
+ } else if (rebuild) {
+ let fragment = rebuildAncestors(node);
+ parent = findElement(node.parentNode, fragment);
+ if (!parent) {
+ dest.appendChild(clone);
+ } else if (breakToken && isText(breakToken.node) && breakToken.offset > 0) {
+ clone.textContent = clone.textContent.substring(breakToken.offset);
+ parent.appendChild(clone);
+ } else {
+ parent.appendChild(clone);
+ }
+
+ dest.appendChild(fragment);
+ } else {
+ dest.appendChild(clone);
+ }
+
+
+ } else {
+ dest.appendChild(clone);
+ }
+
+ if (clone.dataset && clone.dataset.ref) {
+ if (!dest.indexOfRefs) {
+ dest.indexOfRefs = {};
+ }
+ dest.indexOfRefs[clone.dataset.ref] = clone;
+ }
+
+ let nodeHooks = this.hooks.renderNode.triggerSync(clone, node, this);
+ nodeHooks.forEach((newNode) => {
+ if (typeof newNode != "undefined") {
+ clone = newNode;
+ }
+ });
+
+ return clone;
+ }
+
+ rebuildTableFromBreakToken(breakToken, dest) {
+ if (!breakToken || !breakToken.node) {
+ return;
+ }
+ let node = breakToken.node;
+ let td = isElement(node) ? node.closest("td") : node.parentElement.closest("td");
+ if (td) {
+ let rendered = findElement(td, dest, true);
+ if (!rendered) {
+ return;
+ }
+ while ((td = td.nextElementSibling)) {
+ this.append(td, dest, null, true);
+ }
+ }
+ }
+
+ async waitForImages(imgs) {
+ let results = Array.from(imgs).map(async (img) => {
+ return this.awaitImageLoaded(img);
+ });
+ await Promise.all(results);
+ }
+
+ async awaitImageLoaded(image) {
+ return new Promise(resolve => {
+ if (image.complete !== true) {
+ image.onload = function () {
+ let {width, height} = window.getComputedStyle(image);
+ resolve(width, height);
+ };
+ image.onerror = function (e) {
+ let {width, height} = window.getComputedStyle(image);
+ resolve(width, height, e);
+ };
+ } else {
+ let {width, height} = window.getComputedStyle(image);
+ resolve(width, height);
+ }
+ });
+ }
+
+ avoidBreakInside(node, limiter) {
+ let breakNode;
+
+ if (node === limiter) {
+ return;
+ }
+
+ while (node.parentNode) {
+ node = node.parentNode;
+
+ if (node === limiter) {
+ break;
+ }
+
+ if (window.getComputedStyle(node)["break-inside"] === "avoid") {
+ breakNode = node;
+ break;
+ }
+
+ }
+ return breakNode;
+ }
+
+ createBreakToken(overflow, rendered, source) {
+ let container = overflow.startContainer;
+ let offset = overflow.startOffset;
+ let node, renderedNode, parent, index, temp;
+
+ if (isElement(container)) {
+ temp = child(container, offset);
+
+ if (isElement(temp)) {
+ renderedNode = findElement(temp, rendered);
+
+ if (!renderedNode) {
+ // Find closest element with data-ref
+ let prevNode = prevValidNode(temp);
+ if (!isElement(prevNode)) {
+ prevNode = prevNode.parentElement;
+ }
+ renderedNode = findElement(prevNode, rendered);
+ // Check if temp is the last rendered node at its level.
+ if (!temp.nextSibling) {
+ // We need to ensure that the previous sibling of temp is fully rendered.
+ const renderedNodeFromSource = findElement(renderedNode, source);
+ const walker = document.createTreeWalker(renderedNodeFromSource, NodeFilter.SHOW_ELEMENT);
+ const lastChildOfRenderedNodeFromSource = walker.lastChild();
+ const lastChildOfRenderedNodeMatchingFromRendered = findElement(lastChildOfRenderedNodeFromSource, rendered);
+ // Check if we found that the last child in source
+ if (!lastChildOfRenderedNodeMatchingFromRendered) {
+ // Pending content to be rendered before virtual break token
+ return;
+ }
+ // Otherwise we will return a break token as per below
+ }
+ // renderedNode is actually the last unbroken box that does not overflow.
+ // Break Token is therefore the next sibling of renderedNode within source node.
+ node = findElement(renderedNode, source).nextSibling;
+ offset = 0;
+ } else {
+ node = findElement(renderedNode, source);
+ offset = 0;
+ }
+ } else {
+ renderedNode = findElement(container, rendered);
+
+ if (!renderedNode) {
+ renderedNode = findElement(prevValidNode(container), rendered);
+ }
+
+ parent = findElement(renderedNode, source);
+ index = indexOfTextNode(temp, parent);
+ // No seperatation for the first textNode of an element
+ if(index === 0) {
+ node = parent;
+ offset = 0;
+ } else {
+ node = child(parent, index);
+ offset = 0;
+ }
+ }
+ } else {
+ renderedNode = findElement(container.parentNode, rendered);
+
+ if (!renderedNode) {
+ renderedNode = findElement(prevValidNode(container.parentNode), rendered);
+ }
+
+ parent = findElement(renderedNode, source);
+ index = indexOfTextNode(container, parent);
+
+ if (index === -1) {
+ return;
+ }
+
+ node = child(parent, index);
+
+ offset += node.textContent.indexOf(container.textContent);
+ }
+
+ if (!node) {
+ return;
+ }
+
+ return new BreakToken(
+ node,
+ offset
+ );
+
+ }
+
+ findBreakToken(rendered, source, bounds = this.bounds, prevBreakToken, extract = true) {
+ let overflow = this.findOverflow(rendered, bounds);
+ let breakToken, breakLetter;
+
+ let overflowHooks = this.hooks.onOverflow.triggerSync(overflow, rendered, bounds, this);
+ overflowHooks.forEach((newOverflow) => {
+ if (typeof newOverflow != "undefined") {
+ overflow = newOverflow;
+ }
+ });
+
+ if (overflow) {
+ breakToken = this.createBreakToken(overflow, rendered, source);
+ // breakToken is nullable
+ let breakHooks = this.hooks.onBreakToken.triggerSync(breakToken, overflow, rendered, this);
+ breakHooks.forEach((newToken) => {
+ if (typeof newToken != "undefined") {
+ breakToken = newToken;
+ }
+ });
+
+ // Stop removal if we are in a loop
+ if (breakToken && breakToken.equals(prevBreakToken)) {
+ return breakToken;
+ }
+
+ if (breakToken && breakToken["node"] && breakToken["offset"] && breakToken["node"].textContent) {
+ breakLetter = breakToken["node"].textContent.charAt(breakToken["offset"]);
+ } else {
+ breakLetter = undefined;
+ }
+
+ if (breakToken && breakToken.node && extract) {
+ let removed = this.removeOverflow(overflow, breakLetter);
+ this.hooks && this.hooks.afterOverflowRemoved.trigger(removed, rendered, this);
+ }
+
+ }
+ return breakToken;
+ }
+
+ hasOverflow(element, bounds = this.bounds) {
+ let constrainingElement = element && element.parentNode; // this gets the element, instead of the wrapper for the width workaround
+ let {width, height} = element.getBoundingClientRect();
+ let scrollWidth = constrainingElement ? constrainingElement.scrollWidth : 0;
+ let scrollHeight = constrainingElement ? constrainingElement.scrollHeight : 0;
+ return Math.max(Math.floor(width), scrollWidth) > Math.round(bounds.width) ||
+ Math.max(Math.floor(height), scrollHeight) > Math.round(bounds.height);
+ }
+
+ findOverflow(rendered, bounds = this.bounds, gap = this.gap) {
+ if (!this.hasOverflow(rendered, bounds)) return;
+
+ let start = Math.floor(bounds.left);
+ let end = Math.round(bounds.right + gap);
+ let vStart = Math.round(bounds.top);
+ let vEnd = Math.round(bounds.bottom);
+ let range;
+
+ let walker = walk$2(rendered.firstChild, rendered);
+
+ // Find Start
+ let next, done, node, offset, skip, breakAvoid, prev, br;
+ while (!done) {
+ next = walker.next();
+ done = next.done;
+ node = next.value;
+ skip = false;
+ breakAvoid = false;
+ prev = undefined;
+ br = undefined;
+
+ if (node) {
+ let pos = getBoundingClientRect(node);
+ let left = Math.round(pos.left);
+ let right = Math.floor(pos.right);
+ let top = Math.round(pos.top);
+ let bottom = Math.floor(pos.bottom);
+
+ if (!range && (left >= end || top >= vEnd)) {
+ // Check if it is a float
+ let isFloat = false;
+
+ // Check if the node is inside a break-inside: avoid table cell
+ const insideTableCell = parentOf(node, "TD", rendered);
+ if (insideTableCell && window.getComputedStyle(insideTableCell)["break-inside"] === "avoid") {
+ // breaking inside a table cell produces unexpected result, as a workaround, we forcibly avoid break inside in a cell.
+ // But we take the whole row, not just the cell that is causing the break.
+ prev = insideTableCell.parentElement;
+ } else if (isElement(node)) {
+ let styles = window.getComputedStyle(node);
+ isFloat = styles.getPropertyValue("float") !== "none";
+ skip = styles.getPropertyValue("break-inside") === "avoid";
+ breakAvoid = node.dataset.breakBefore === "avoid" || node.dataset.previousBreakAfter === "avoid";
+ prev = breakAvoid && nodeBefore(node, rendered);
+ br = node.tagName === "BR" || node.tagName === "WBR";
+ }
+
+ let tableRow;
+ if (node.nodeName === "TR") {
+ tableRow = node;
+ } else {
+ tableRow = parentOf(node, "TR", rendered);
+ }
+ if (tableRow) {
+ // honor break-inside="avoid" in parent tbody/thead
+ let container = tableRow.parentElement;
+ if (["TBODY", "THEAD"].includes(container.nodeName)) {
+ let styles = window.getComputedStyle(container);
+ if (styles.getPropertyValue("break-inside") === "avoid") prev = container;
+ }
+
+ // Check if the node is inside a row with a rowspan
+ const table = parentOf(tableRow, "TABLE", rendered);
+ const rowspan = table.querySelector("[colspan]");
+ if (table && rowspan) {
+ let columnCount = 0;
+ for (const cell of Array.from(table.rows[0].cells)) {
+ columnCount += parseInt(cell.getAttribute("colspan") || "1");
+ }
+ if (tableRow.cells.length !== columnCount) {
+ let previousRow = tableRow.previousElementSibling;
+ let previousRowColumnCount;
+ while (previousRow !== null) {
+ previousRowColumnCount = 0;
+ for (const cell of Array.from(previousRow.cells)) {
+ previousRowColumnCount += parseInt(cell.getAttribute("colspan") || "1");
+ }
+ if (previousRowColumnCount === columnCount) {
+ break;
+ }
+ previousRow = previousRow.previousElementSibling;
+ }
+ if (previousRowColumnCount === columnCount) {
+ prev = previousRow;
+ }
+ }
+ }
+ }
+
+ if (prev) {
+ range = document.createRange();
+ range.selectNode(prev);
+ break;
+ }
+
+ if (!br && !isFloat && isElement(node)) {
+ range = document.createRange();
+ range.selectNode(node);
+ break;
+ }
+
+ if (isText(node) && node.textContent.trim().length) {
+ range = document.createRange();
+ range.selectNode(node);
+ break;
+ }
+
+ }
+
+ if (!range && isText(node) &&
+ node.textContent.trim().length &&
+ !breakInsideAvoidParentNode(node.parentNode)) {
+
+ let rects = getClientRects(node);
+ let rect;
+ left = 0;
+ top = 0;
+ for (var i = 0; i != rects.length; i++) {
+ rect = rects[i];
+ if (rect.width > 0 && (!left || rect.left > left)) {
+ left = rect.left;
+ }
+ if (rect.height > 0 && (!top || rect.top > top)) {
+ top = rect.top;
+ }
+ }
+
+ if (left >= end || top >= vEnd) {
+ range = document.createRange();
+ offset = this.textBreak(node, start, end, vStart, vEnd);
+ if (!offset) {
+ range = undefined;
+ } else {
+ range.setStart(node, offset);
+ }
+ break;
+ }
+ }
+
+ // Skip children
+ if (skip || (right <= end && bottom <= vEnd)) {
+ next = nodeAfter(node, rendered);
+ if (next) {
+ walker = walk$2(next, rendered);
+ }
+
+ }
+
+ }
+ }
+
+ // Find End
+ if (range) {
+ range.setEndAfter(rendered.lastChild);
+ return range;
+ }
+
+ }
+
+ findEndToken(rendered, source) {
+ if (rendered.childNodes.length === 0) {
+ return;
+ }
+
+ let lastChild = rendered.lastChild;
+
+ let lastNodeIndex;
+ while (lastChild && lastChild.lastChild) {
+ if (!validNode(lastChild)) {
+ // Only get elements with refs
+ lastChild = lastChild.previousSibling;
+ } else if (!validNode(lastChild.lastChild)) {
+ // Deal with invalid dom items
+ lastChild = prevValidNode(lastChild.lastChild);
+ break;
+ } else {
+ lastChild = lastChild.lastChild;
+ }
+ }
+
+ if (isText(lastChild)) {
+
+ if (lastChild.parentNode.dataset.ref) {
+ lastNodeIndex = indexOf$2(lastChild);
+ lastChild = lastChild.parentNode;
+ } else {
+ lastChild = lastChild.previousSibling;
+ }
+ }
+
+ let original = findElement(lastChild, source);
+
+ if (lastNodeIndex) {
+ original = original.childNodes[lastNodeIndex];
+ }
+
+ let after = nodeAfter(original);
+
+ return this.breakAt(after);
+ }
+
+ textBreak(node, start, end, vStart, vEnd) {
+ let wordwalker = words(node);
+ let left = 0;
+ let right = 0;
+ let top = 0;
+ let bottom = 0;
+ let word, next, done, pos;
+ let offset;
+ while (!done) {
+ next = wordwalker.next();
+ word = next.value;
+ done = next.done;
+
+ if (!word) {
+ break;
+ }
+
+ pos = getBoundingClientRect(word);
+
+ left = Math.floor(pos.left);
+ right = Math.floor(pos.right);
+ top = Math.floor(pos.top);
+ bottom = Math.floor(pos.bottom);
+
+ if (left >= end || top >= vEnd) {
+ offset = word.startOffset;
+ break;
+ }
+
+ if (right > end || bottom > vEnd) {
+ let letterwalker = letters(word);
+ let letter, nextLetter, doneLetter;
+
+ while (!doneLetter) {
+ nextLetter = letterwalker.next();
+ letter = nextLetter.value;
+ doneLetter = nextLetter.done;
+
+ if (!letter) {
+ break;
+ }
+
+ pos = getBoundingClientRect(letter);
+ left = Math.floor(pos.left);
+ top = Math.floor(pos.top);
+
+ if (left >= end || top >= vEnd) {
+ offset = letter.startOffset;
+ done = true;
+
+ break;
+ }
+ }
+ }
+
+ }
+
+ return offset;
+ }
+
+ removeOverflow(overflow, breakLetter) {
+ let {startContainer} = overflow;
+ let extracted = overflow.extractContents();
+
+ this.hyphenateAtBreak(startContainer, breakLetter);
+
+ return extracted;
+ }
+
+ hyphenateAtBreak(startContainer, breakLetter) {
+ if (isText(startContainer)) {
+ let startText = startContainer.textContent;
+ let prevLetter = startText[startText.length - 1];
+
+ // Add a hyphen if previous character is a letter or soft hyphen
+ if (
+ (breakLetter && /^\w|\u00AD$/.test(prevLetter) && /^\w|\u00AD$/.test(breakLetter)) ||
+ (!breakLetter && /^\w|\u00AD$/.test(prevLetter))
+ ) {
+ startContainer.parentNode.classList.add("pagedjs_hyphen");
+ startContainer.textContent += this.settings.hyphenGlyph || "\u2011";
+ }
+ }
+ }
+
+ equalTokens(a, b) {
+ if (!a || !b) {
+ return false;
+ }
+ if (a["node"] && b["node"] && a["node"] !== b["node"]) {
+ return false;
+ }
+ if (a["offset"] && b["offset"] && a["offset"] !== b["offset"]) {
+ return false;
+ }
+ return true;
+ }
+ }
+
+ EventEmitter(Layout.prototype);
+
+ /**
+ * Render a page
+ * @class
+ */
+ class Page {
+ constructor(pagesArea, pageTemplate, blank, hooks, options) {
+ this.pagesArea = pagesArea;
+ this.pageTemplate = pageTemplate;
+ this.blank = blank;
+
+ this.width = undefined;
+ this.height = undefined;
+
+ this.hooks = hooks;
+
+ this.settings = options || {};
+
+ // this.element = this.create(this.pageTemplate);
+ }
+
+ create(template, after) {
+ //let documentFragment = document.createRange().createContextualFragment( TEMPLATE );
+ //let page = documentFragment.children[0];
+ let clone = document.importNode(this.pageTemplate.content, true);
+
+ let page, index;
+ if (after) {
+ this.pagesArea.insertBefore(clone, after.nextElementSibling);
+ index = Array.prototype.indexOf.call(this.pagesArea.children, after.nextElementSibling);
+ page = this.pagesArea.children[index];
+ } else {
+ this.pagesArea.appendChild(clone);
+ page = this.pagesArea.lastChild;
+ }
+
+ let pagebox = page.querySelector(".pagedjs_pagebox");
+ let area = page.querySelector(".pagedjs_page_content");
+ let footnotesArea = page.querySelector(".pagedjs_footnote_area");
+
+
+ let size = area.getBoundingClientRect();
+
+
+ area.style.columnWidth = Math.round(size.width) + "px";
+ area.style.columnGap = "calc(var(--pagedjs-margin-right) + var(--pagedjs-margin-left) + var(--pagedjs-bleed-right) + var(--pagedjs-bleed-left) + var(--pagedjs-column-gap-offset))";
+ // area.style.overflow = "scroll";
+
+ this.width = Math.round(size.width);
+ this.height = Math.round(size.height);
+
+ this.element = page;
+ this.pagebox = pagebox;
+ this.area = area;
+ this.footnotesArea = footnotesArea;
+
+ return page;
+ }
+
+ createWrapper() {
+ let wrapper = document.createElement("div");
+
+ this.area.appendChild(wrapper);
+
+ this.wrapper = wrapper;
+
+ return wrapper;
+ }
+
+ index(pgnum) {
+ this.position = pgnum;
+
+ let page = this.element;
+ // let pagebox = this.pagebox;
+
+ let index = pgnum + 1;
+
+ let id = `page-${index}`;
+
+ this.id = id;
+
+ // page.dataset.pageNumber = index;
+
+ page.dataset.pageNumber = index;
+ page.setAttribute("id", id);
+
+ if (this.name) {
+ page.classList.add("pagedjs_" + this.name + "_page");
+ }
+
+ if (this.blank) {
+ page.classList.add("pagedjs_blank_page");
+ }
+
+ if (pgnum === 0) {
+ page.classList.add("pagedjs_first_page");
+ }
+
+ if (pgnum % 2 !== 1) {
+ page.classList.remove("pagedjs_left_page");
+ page.classList.add("pagedjs_right_page");
+ } else {
+ page.classList.remove("pagedjs_right_page");
+ page.classList.add("pagedjs_left_page");
+ }
+ }
+
+ /*
+ size(width, height) {
+ if (width === this.width && height === this.height) {
+ return;
+ }
+ this.width = width;
+ this.height = height;
+
+ this.element.style.width = Math.round(width) + "px";
+ this.element.style.height = Math.round(height) + "px";
+ this.element.style.columnWidth = Math.round(width) + "px";
+ }
+ */
+
+ async layout(contents, breakToken, maxChars) {
+
+ this.clear();
+
+ this.startToken = breakToken;
+
+ let settings = this.settings;
+ if (!settings.maxChars && maxChars) {
+ settings.maxChars = maxChars;
+ }
+
+ this.layoutMethod = new Layout(this.area, this.hooks, settings);
+
+ let renderResult = await this.layoutMethod.renderTo(this.wrapper, contents, breakToken);
+ let newBreakToken = renderResult.breakToken;
+
+ this.addListeners(contents);
+
+ this.endToken = newBreakToken;
+
+ return newBreakToken;
+ }
+
+ async append(contents, breakToken) {
+
+ if (!this.layoutMethod) {
+ return this.layout(contents, breakToken);
+ }
+
+ let renderResult = await this.layoutMethod.renderTo(this.wrapper, contents, breakToken);
+ let newBreakToken = renderResult.breakToken;
+
+ this.endToken = newBreakToken;
+
+ return newBreakToken;
+ }
+
+ getByParent(ref, entries) {
+ let e;
+ for (var i = 0; i < entries.length; i++) {
+ e = entries[i];
+ if (e.dataset.ref === ref) {
+ return e;
+ }
+ }
+ }
+
+ onOverflow(func) {
+ this._onOverflow = func;
+ }
+
+ onUnderflow(func) {
+ this._onUnderflow = func;
+ }
+
+ clear() {
+ this.removeListeners();
+ this.wrapper && this.wrapper.remove();
+ this.createWrapper();
+ }
+
+ addListeners(contents) {
+ if (typeof ResizeObserver !== "undefined") {
+ this.addResizeObserver(contents);
+ } else {
+ this._checkOverflowAfterResize = this.checkOverflowAfterResize.bind(this, contents);
+ this.element.addEventListener("overflow", this._checkOverflowAfterResize, false);
+ this.element.addEventListener("underflow", this._checkOverflowAfterResize, false);
+ }
+ // TODO: fall back to mutation observer?
+
+ this._onScroll = function () {
+ if (this.listening) {
+ this.element.scrollLeft = 0;
+ }
+ }.bind(this);
+
+ // Keep scroll left from changing
+ this.element.addEventListener("scroll", this._onScroll);
+
+ this.listening = true;
+
+ return true;
+ }
+
+ removeListeners() {
+ this.listening = false;
+
+ if (typeof ResizeObserver !== "undefined" && this.ro) {
+ this.ro.disconnect();
+ } else if (this.element) {
+ this.element.removeEventListener("overflow", this._checkOverflowAfterResize, false);
+ this.element.removeEventListener("underflow", this._checkOverflowAfterResize, false);
+ }
+
+ this.element && this.element.removeEventListener("scroll", this._onScroll);
+
+ }
+
+ addResizeObserver(contents) {
+ let wrapper = this.wrapper;
+ let prevHeight = wrapper.getBoundingClientRect().height;
+ this.ro = new ResizeObserver(entries => {
+
+ if (!this.listening) {
+ return;
+ }
+ requestAnimationFrame(() => {
+ for (let entry of entries) {
+ const cr = entry.contentRect;
+
+ if (cr.height > prevHeight) {
+ this.checkOverflowAfterResize(contents);
+ prevHeight = wrapper.getBoundingClientRect().height;
+ } else if (cr.height < prevHeight) { // TODO: calc line height && (prevHeight - cr.height) >= 22
+ this.checkUnderflowAfterResize(contents);
+ prevHeight = cr.height;
+ }
+ }
+ });
+ });
+
+ this.ro.observe(wrapper);
+ }
+
+ checkOverflowAfterResize(contents) {
+ if (!this.listening || !this.layoutMethod) {
+ return;
+ }
+
+ let newBreakToken = this.layoutMethod.findBreakToken(this.wrapper, contents, this.startToken);
+
+ if (newBreakToken) {
+ this.endToken = newBreakToken;
+ this._onOverflow && this._onOverflow(newBreakToken);
+ }
+ }
+
+ checkUnderflowAfterResize(contents) {
+ if (!this.listening || !this.layoutMethod) {
+ return;
+ }
+
+ let endToken = this.layoutMethod.findEndToken(this.wrapper, contents);
+
+ if (endToken) {
+ this._onUnderflow && this._onUnderflow(endToken);
+ }
+ }
+
+
+ destroy() {
+ this.removeListeners();
+
+ this.element.remove();
+
+ this.element = undefined;
+ this.wrapper = undefined;
+ }
+ }
+
+ EventEmitter(Page.prototype);
+
+ /**
+ * Render a flow of text offscreen
+ * @class
+ */
+ class ContentParser {
+
+ constructor(content, cb) {
+ if (content && content.nodeType) {
+ // handle dom
+ this.dom = this.add(content);
+ } else if (typeof content === "string") {
+ this.dom = this.parse(content);
+ }
+
+ return this.dom;
+ }
+
+ parse(markup, mime) {
+ let range = document.createRange();
+ let fragment = range.createContextualFragment(markup);
+
+ this.addRefs(fragment);
+
+ return fragment;
+ }
+
+ add(contents) {
+ // let fragment = document.createDocumentFragment();
+ //
+ // let children = [...contents.childNodes];
+ // for (let child of children) {
+ // let clone = child.cloneNode(true);
+ // fragment.appendChild(clone);
+ // }
+
+ this.addRefs(contents);
+
+ return contents;
+ }
+
+ addRefs(content) {
+ var treeWalker = document.createTreeWalker(
+ content,
+ NodeFilter.SHOW_ELEMENT,
+ null,
+ false
+ );
+
+ let node = treeWalker.nextNode();
+ while(node) {
+
+ if (!node.hasAttribute("data-ref")) {
+ let uuid = UUID();
+ node.setAttribute("data-ref", uuid);
+ }
+
+ if (node.id) {
+ node.setAttribute("data-id", node.id);
+ }
+
+ // node.setAttribute("data-children", node.childNodes.length);
+
+ // node.setAttribute("data-text", node.textContent.trim().length);
+ node = treeWalker.nextNode();
+ }
+ }
+
+ find(ref) {
+ return this.refs[ref];
+ }
+
+ destroy() {
+ this.refs = undefined;
+ this.dom = undefined;
+ }
+ }
+
+ /**
+ * Queue for handling tasks one at a time
+ * @class
+ * @param {scope} context what this will resolve to in the tasks
+ */
+ class Queue {
+ constructor(context){
+ this._q = [];
+ this.context = context;
+ this.tick = requestAnimationFrame;
+ this.running = false;
+ this.paused = false;
+ }
+
+ /**
+ * Add an item to the queue
+ * @return {Promise} enqueued
+ */
+ enqueue() {
+ var deferred, promise;
+ var queued;
+ var task = [].shift.call(arguments);
+ var args = arguments;
+
+ // Handle single args without context
+ // if(args && !Array.isArray(args)) {
+ // args = [args];
+ // }
+ if(!task) {
+ throw new Error("No Task Provided");
+ }
+
+ if(typeof task === "function"){
+
+ deferred = new defer();
+ promise = deferred.promise;
+
+ queued = {
+ "task" : task,
+ "args" : args,
+ //"context" : context,
+ "deferred" : deferred,
+ "promise" : promise
+ };
+
+ } else {
+ // Task is a promise
+ queued = {
+ "promise" : task
+ };
+
+ }
+
+ this._q.push(queued);
+
+ // Wait to start queue flush
+ if (this.paused == false && !this.running) {
+ this.run();
+ }
+
+ return queued.promise;
+ }
+
+ /**
+ * Run one item
+ * @return {Promise} dequeued
+ */
+ dequeue(){
+ var inwait, task, result;
+
+ if(this._q.length && !this.paused) {
+ inwait = this._q.shift();
+ task = inwait.task;
+ if(task){
+ // console.log(task)
+
+ result = task.apply(this.context, inwait.args);
+
+ if(result && typeof result["then"] === "function") {
+ // Task is a function that returns a promise
+ return result.then(function(){
+ inwait.deferred.resolve.apply(this.context, arguments);
+ }.bind(this), function() {
+ inwait.deferred.reject.apply(this.context, arguments);
+ }.bind(this));
+ } else {
+ // Task resolves immediately
+ inwait.deferred.resolve.apply(this.context, result);
+ return inwait.promise;
+ }
+
+
+
+ } else if(inwait.promise) {
+ // Task is a promise
+ return inwait.promise;
+ }
+
+ } else {
+ inwait = new defer();
+ inwait.deferred.resolve();
+ return inwait.promise;
+ }
+
+ }
+
+ // Run All Immediately
+ dump(){
+ while(this._q.length) {
+ this.dequeue();
+ }
+ }
+
+ /**
+ * Run all tasks sequentially, at convince
+ * @return {Promise} all run
+ */
+ run(){
+
+ if(!this.running){
+ this.running = true;
+ this.defered = new defer();
+ }
+
+ this.tick.call(window, () => {
+
+ if(this._q.length) {
+
+ this.dequeue()
+ .then(function(){
+ this.run();
+ }.bind(this));
+
+ } else {
+ this.defered.resolve();
+ this.running = undefined;
+ }
+
+ });
+
+ // Unpause
+ if(this.paused == true) {
+ this.paused = false;
+ }
+
+ return this.defered.promise;
+ }
+
+ /**
+ * Flush all, as quickly as possible
+ * @return {Promise} ran
+ */
+ flush(){
+
+ if(this.running){
+ return this.running;
+ }
+
+ if(this._q.length) {
+ this.running = this.dequeue()
+ .then(function(){
+ this.running = undefined;
+ return this.flush();
+ }.bind(this));
+
+ return this.running;
+ }
+
+ }
+
+ /**
+ * Clear all items in wait
+ * @return {void}
+ */
+ clear(){
+ this._q = [];
+ }
+
+ /**
+ * Get the number of tasks in the queue
+ * @return {number} tasks
+ */
+ length(){
+ return this._q.length;
+ }
+
+ /**
+ * Pause a running queue
+ * @return {void}
+ */
+ pause(){
+ this.paused = true;
+ }
+
+ /**
+ * End the queue
+ * @return {void}
+ */
+ stop(){
+ this._q = [];
+ this.running = false;
+ this.paused = true;
+ }
+ }
+
+ const TEMPLATE = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`;
+
+ /**
+ * Chop up text into flows
+ * @class
+ */
+ class Chunker {
+ constructor(content, renderTo, options) {
+ // this.preview = preview;
+
+ this.settings = options || {};
+
+ this.hooks = {};
+ this.hooks.beforeParsed = new Hook(this);
+ this.hooks.filter = new Hook(this);
+ this.hooks.afterParsed = new Hook(this);
+ this.hooks.beforePageLayout = new Hook(this);
+ this.hooks.onPageLayout = new Hook(this);
+ this.hooks.layout = new Hook(this);
+ this.hooks.renderNode = new Hook(this);
+ this.hooks.layoutNode = new Hook(this);
+ this.hooks.onOverflow = new Hook(this);
+ this.hooks.afterOverflowRemoved = new Hook(this);
+ this.hooks.onBreakToken = new Hook();
+ this.hooks.beforeRenderResult = new Hook(this);
+ this.hooks.afterPageLayout = new Hook(this);
+ this.hooks.finalizePage = new Hook(this);
+ this.hooks.afterRendered = new Hook(this);
+
+ this.pages = [];
+ this.total = 0;
+
+ this.q = new Queue(this);
+ this.stopped = false;
+ this.rendered = false;
+
+ this.content = content;
+
+ this.charsPerBreak = [];
+ this.maxChars;
+
+ if (content) {
+ this.flow(content, renderTo);
+ }
+ }
+
+ setup(renderTo) {
+ this.pagesArea = document.createElement("div");
+ this.pagesArea.classList.add("pagedjs_pages");
+
+ if (renderTo) {
+ renderTo.appendChild(this.pagesArea);
+ } else {
+ document.querySelector("body").appendChild(this.pagesArea);
+ }
+
+ this.pageTemplate = document.createElement("template");
+ this.pageTemplate.innerHTML = TEMPLATE;
+
+ }
+
+ async flow(content, renderTo) {
+ let parsed;
+
+ await this.hooks.beforeParsed.trigger(content, this);
+
+ parsed = new ContentParser(content);
+
+ this.hooks.filter.triggerSync(parsed);
+
+ this.source = parsed;
+ this.breakToken = undefined;
+
+ if (this.pagesArea && this.pageTemplate) {
+ this.q.clear();
+ this.removePages();
+ } else {
+ this.setup(renderTo);
+ }
+
+ this.emit("rendering", parsed);
+
+ await this.hooks.afterParsed.trigger(parsed, this);
+
+ await this.loadFonts();
+
+ let rendered = await this.render(parsed, this.breakToken);
+ while (rendered.canceled) {
+ this.start();
+ rendered = await this.render(parsed, this.breakToken);
+ }
+
+ this.rendered = true;
+ this.pagesArea.style.setProperty("--pagedjs-page-count", this.total);
+
+ await this.hooks.afterRendered.trigger(this.pages, this);
+
+ this.emit("rendered", this.pages);
+
+
+
+ return this;
+ }
+
+ // oversetPages() {
+ // let overset = [];
+ // for (let i = 0; i < this.pages.length; i++) {
+ // let page = this.pages[i];
+ // if (page.overset) {
+ // overset.push(page);
+ // // page.overset = false;
+ // }
+ // }
+ // return overset;
+ // }
+ //
+ // async handleOverset(parsed) {
+ // let overset = this.oversetPages();
+ // if (overset.length) {
+ // console.log("overset", overset);
+ // let index = this.pages.indexOf(overset[0]) + 1;
+ // console.log("INDEX", index);
+ //
+ // // Remove pages
+ // // this.removePages(index);
+ //
+ // // await this.render(parsed, overset[0].overset);
+ //
+ // // return this.handleOverset(parsed);
+ // }
+ // }
+
+ async render(parsed, startAt) {
+ let renderer = this.layout(parsed, startAt);
+
+ let done = false;
+ let result;
+ while (!done) {
+ result = await this.q.enqueue(() => { return this.renderAsync(renderer); });
+ done = result.done;
+ }
+
+ return result;
+ }
+
+ start() {
+ this.rendered = false;
+ this.stopped = false;
+ }
+
+ stop() {
+ this.stopped = true;
+ // this.q.clear();
+ }
+
+ renderOnIdle(renderer) {
+ return new Promise(resolve => {
+ requestIdleCallback(async () => {
+ if (this.stopped) {
+ return resolve({ done: true, canceled: true });
+ }
+ let result = await renderer.next();
+ if (this.stopped) {
+ resolve({ done: true, canceled: true });
+ } else {
+ resolve(result);
+ }
+ });
+ });
+ }
+
+ async renderAsync(renderer) {
+ if (this.stopped) {
+ return { done: true, canceled: true };
+ }
+ let result = await renderer.next();
+ if (this.stopped) {
+ return { done: true, canceled: true };
+ } else {
+ return result;
+ }
+ }
+
+ async handleBreaks(node, force) {
+ let currentPage = this.total + 1;
+ let currentPosition = currentPage % 2 === 0 ? "left" : "right";
+ // TODO: Recto and Verso should reverse for rtl languages
+ let currentSide = currentPage % 2 === 0 ? "verso" : "recto";
+ let previousBreakAfter;
+ let breakBefore;
+ let page;
+
+ if (currentPage === 1) {
+ return;
+ }
+
+ if (node &&
+ typeof node.dataset !== "undefined" &&
+ typeof node.dataset.previousBreakAfter !== "undefined") {
+ previousBreakAfter = node.dataset.previousBreakAfter;
+ }
+
+ if (node &&
+ typeof node.dataset !== "undefined" &&
+ typeof node.dataset.breakBefore !== "undefined") {
+ breakBefore = node.dataset.breakBefore;
+ }
+
+ if (force) {
+ page = this.addPage(true);
+ } else if( previousBreakAfter &&
+ (previousBreakAfter === "left" || previousBreakAfter === "right") &&
+ previousBreakAfter !== currentPosition) {
+ page = this.addPage(true);
+ } else if( previousBreakAfter &&
+ (previousBreakAfter === "verso" || previousBreakAfter === "recto") &&
+ previousBreakAfter !== currentSide) {
+ page = this.addPage(true);
+ } else if( breakBefore &&
+ (breakBefore === "left" || breakBefore === "right") &&
+ breakBefore !== currentPosition) {
+ page = this.addPage(true);
+ } else if( breakBefore &&
+ (breakBefore === "verso" || breakBefore === "recto") &&
+ breakBefore !== currentSide) {
+ page = this.addPage(true);
+ }
+
+ if (page) {
+ await this.hooks.beforePageLayout.trigger(page, undefined, undefined, this);
+ this.emit("page", page);
+ // await this.hooks.layout.trigger(page.element, page, undefined, this);
+ await this.hooks.afterPageLayout.trigger(page.element, page, undefined, this);
+ await this.hooks.finalizePage.trigger(page.element, page, undefined, this);
+ this.emit("renderedPage", page);
+ }
+ }
+
+ async *layout(content, startAt) {
+ let breakToken = startAt || false;
+ let tokens = [];
+
+ while (breakToken !== undefined && (true)) {
+
+ if (breakToken && breakToken.node) {
+ await this.handleBreaks(breakToken.node);
+ } else {
+ await this.handleBreaks(content.firstChild);
+ }
+
+ let page = this.addPage();
+
+ await this.hooks.beforePageLayout.trigger(page, content, breakToken, this);
+ this.emit("page", page);
+
+ // Layout content in the page, starting from the breakToken
+ breakToken = await page.layout(content, breakToken, this.maxChars);
+
+ if (breakToken) {
+ let newToken = breakToken.toJSON(true);
+ if (tokens.lastIndexOf(newToken) > -1) {
+ // loop
+ let err = new OverflowContentError("Layout repeated", [breakToken.node]);
+ console.error("Layout repeated at: ", breakToken.node);
+ return err;
+ } else {
+ tokens.push(newToken);
+ }
+ }
+
+ await this.hooks.afterPageLayout.trigger(page.element, page, breakToken, this);
+ await this.hooks.finalizePage.trigger(page.element, page, undefined, this);
+ this.emit("renderedPage", page);
+
+ this.recoredCharLength(page.wrapper.textContent.length);
+
+ yield breakToken;
+
+ // Stop if we get undefined, showing we have reached the end of the content
+ }
+
+
+ }
+
+ recoredCharLength(length) {
+ if (length === 0) {
+ return;
+ }
+
+ this.charsPerBreak.push(length);
+
+ // Keep the length of the last few breaks
+ if (this.charsPerBreak.length > 4) {
+ this.charsPerBreak.shift();
+ }
+
+ this.maxChars = this.charsPerBreak.reduce((a, b) => a + b, 0) / (this.charsPerBreak.length);
+ }
+
+ removePages(fromIndex=0) {
+
+ if (fromIndex >= this.pages.length) {
+ return;
+ }
+
+ // Remove pages
+ for (let i = fromIndex; i < this.pages.length; i++) {
+ this.pages[i].destroy();
+ }
+
+ if (fromIndex > 0) {
+ this.pages.splice(fromIndex);
+ } else {
+ this.pages = [];
+ }
+
+ this.total = this.pages.length;
+ }
+
+ addPage(blank) {
+ let lastPage = this.pages[this.pages.length - 1];
+ // Create a new page from the template
+ let page = new Page(this.pagesArea, this.pageTemplate, blank, this.hooks, this.settings);
+
+ this.pages.push(page);
+
+ // Create the pages
+ page.create(undefined, lastPage && lastPage.element);
+
+ page.index(this.total);
+
+ if (!blank) {
+ // Listen for page overflow
+ page.onOverflow((overflowToken) => {
+ console.warn("overflow on", page.id, overflowToken);
+
+ // Only reflow while rendering
+ if (this.rendered) {
+ return;
+ }
+
+ let index = this.pages.indexOf(page) + 1;
+
+ // Stop the rendering
+ this.stop();
+
+ // Set the breakToken to resume at
+ this.breakToken = overflowToken;
+
+ // Remove pages
+ this.removePages(index);
+
+ if (this.rendered === true) {
+ this.rendered = false;
+
+ this.q.enqueue(async () => {
+
+ this.start();
+
+ await this.render(this.source, this.breakToken);
+
+ this.rendered = true;
+
+ });
+ }
+
+
+ });
+
+ page.onUnderflow((overflowToken) => {
+ // console.log("underflow on", page.id, overflowToken);
+
+ // page.append(this.source, overflowToken);
+
+ });
+ }
+
+ this.total = this.pages.length;
+
+ return page;
+ }
+ /*
+ insertPage(index, blank) {
+ let lastPage = this.pages[index];
+ // Create a new page from the template
+ let page = new Page(this.pagesArea, this.pageTemplate, blank, this.hooks);
+
+ let total = this.pages.splice(index, 0, page);
+
+ // Create the pages
+ page.create(undefined, lastPage && lastPage.element);
+
+ page.index(index + 1);
+
+ for (let i = index + 2; i < this.pages.length; i++) {
+ this.pages[i].index(i);
+ }
+
+ if (!blank) {
+ // Listen for page overflow
+ page.onOverflow((overflowToken) => {
+ if (total < this.pages.length) {
+ this.pages[total].layout(this.source, overflowToken);
+ } else {
+ let newPage = this.addPage();
+ newPage.layout(this.source, overflowToken);
+ }
+ });
+
+ page.onUnderflow(() => {
+ // console.log("underflow on", page.id);
+ });
+ }
+
+ this.total += 1;
+
+ return page;
+ }
+ */
+
+ async clonePage(originalPage) {
+ let lastPage = this.pages[this.pages.length - 1];
+
+ let page = new Page(this.pagesArea, this.pageTemplate, false, this.hooks);
+
+ this.pages.push(page);
+
+ // Create the pages
+ page.create(undefined, lastPage && lastPage.element);
+
+ page.index(this.total);
+
+ await this.hooks.beforePageLayout.trigger(page, undefined, undefined, this);
+ this.emit("page", page);
+
+ for (const className of originalPage.element.classList) {
+ if (className !== "pagedjs_left_page" && className !== "pagedjs_right_page") {
+ page.element.classList.add(className);
+ }
+ }
+
+ await this.hooks.afterPageLayout.trigger(page.element, page, undefined, this);
+ await this.hooks.finalizePage.trigger(page.element, page, undefined, this);
+ this.emit("renderedPage", page);
+ }
+
+ loadFonts() {
+ let fontPromises = [];
+ (document.fonts || []).forEach((fontFace) => {
+ if (fontFace.status !== "loaded") {
+ let fontLoaded = fontFace.load().then((r) => {
+ return fontFace.family;
+ }, (r) => {
+ console.warn("Failed to preload font-family:", fontFace.family);
+ return fontFace.family;
+ });
+ fontPromises.push(fontLoaded);
+ }
+ });
+ return Promise.all(fontPromises).catch((err) => {
+ console.warn(err);
+ });
+ }
+
+ destroy() {
+ this.pagesArea.remove();
+ this.pageTemplate.remove();
+ }
+
+ }
+
+ EventEmitter(Chunker.prototype);
+
+ var syntax = {exports: {}};
+
+ var create$4 = {};
+
+ //
+ // list
+ // ┌──────┐
+ // ┌──────────────┼─head │
+ // │ │ tail─┼──────────────┐
+ // │ └──────┘ │
+ // ▼ ▼
+ // item item item item
+ // ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
+ // null ◀──┼─prev │◀───┼─prev │◀───┼─prev │◀───┼─prev │
+ // │ next─┼───▶│ next─┼───▶│ next─┼───▶│ next─┼──▶ null
+ // ├──────┤ ├──────┤ ├──────┤ ├──────┤
+ // │ data │ │ data │ │ data │ │ data │
+ // └──────┘ └──────┘ └──────┘ └──────┘
+ //
+
+ function createItem(data) {
+ return {
+ prev: null,
+ next: null,
+ data: data
+ };
+ }
+
+ function allocateCursor(node, prev, next) {
+ var cursor;
+
+ if (cursors !== null) {
+ cursor = cursors;
+ cursors = cursors.cursor;
+ cursor.prev = prev;
+ cursor.next = next;
+ cursor.cursor = node.cursor;
+ } else {
+ cursor = {
+ prev: prev,
+ next: next,
+ cursor: node.cursor
+ };
+ }
+
+ node.cursor = cursor;
+
+ return cursor;
+ }
+
+ function releaseCursor(node) {
+ var cursor = node.cursor;
+
+ node.cursor = cursor.cursor;
+ cursor.prev = null;
+ cursor.next = null;
+ cursor.cursor = cursors;
+ cursors = cursor;
+ }
+
+ var cursors = null;
+ var List$6 = function() {
+ this.cursor = null;
+ this.head = null;
+ this.tail = null;
+ };
+
+ List$6.createItem = createItem;
+ List$6.prototype.createItem = createItem;
+
+ List$6.prototype.updateCursors = function(prevOld, prevNew, nextOld, nextNew) {
+ var cursor = this.cursor;
+
+ while (cursor !== null) {
+ if (cursor.prev === prevOld) {
+ cursor.prev = prevNew;
+ }
+
+ if (cursor.next === nextOld) {
+ cursor.next = nextNew;
+ }
+
+ cursor = cursor.cursor;
+ }
+ };
+
+ List$6.prototype.getSize = function() {
+ var size = 0;
+ var cursor = this.head;
+
+ while (cursor) {
+ size++;
+ cursor = cursor.next;
+ }
+
+ return size;
+ };
+
+ List$6.prototype.fromArray = function(array) {
+ var cursor = null;
+
+ this.head = null;
+
+ for (var i = 0; i < array.length; i++) {
+ var item = createItem(array[i]);
+
+ if (cursor !== null) {
+ cursor.next = item;
+ } else {
+ this.head = item;
+ }
+
+ item.prev = cursor;
+ cursor = item;
+ }
+
+ this.tail = cursor;
+
+ return this;
+ };
+
+ List$6.prototype.toArray = function() {
+ var cursor = this.head;
+ var result = [];
+
+ while (cursor) {
+ result.push(cursor.data);
+ cursor = cursor.next;
+ }
+
+ return result;
+ };
+
+ List$6.prototype.toJSON = List$6.prototype.toArray;
+
+ List$6.prototype.isEmpty = function() {
+ return this.head === null;
+ };
+
+ List$6.prototype.first = function() {
+ return this.head && this.head.data;
+ };
+
+ List$6.prototype.last = function() {
+ return this.tail && this.tail.data;
+ };
+
+ List$6.prototype.each = function(fn, context) {
+ var item;
+
+ if (context === undefined) {
+ context = this;
+ }
+
+ // push cursor
+ var cursor = allocateCursor(this, null, this.head);
+
+ while (cursor.next !== null) {
+ item = cursor.next;
+ cursor.next = item.next;
+
+ fn.call(context, item.data, item, this);
+ }
+
+ // pop cursor
+ releaseCursor(this);
+ };
+
+ List$6.prototype.forEach = List$6.prototype.each;
+
+ List$6.prototype.eachRight = function(fn, context) {
+ var item;
+
+ if (context === undefined) {
+ context = this;
+ }
+
+ // push cursor
+ var cursor = allocateCursor(this, this.tail, null);
+
+ while (cursor.prev !== null) {
+ item = cursor.prev;
+ cursor.prev = item.prev;
+
+ fn.call(context, item.data, item, this);
+ }
+
+ // pop cursor
+ releaseCursor(this);
+ };
+
+ List$6.prototype.forEachRight = List$6.prototype.eachRight;
+
+ List$6.prototype.reduce = function(fn, initialValue, context) {
+ var item;
+
+ if (context === undefined) {
+ context = this;
+ }
+
+ // push cursor
+ var cursor = allocateCursor(this, null, this.head);
+ var acc = initialValue;
+
+ while (cursor.next !== null) {
+ item = cursor.next;
+ cursor.next = item.next;
+
+ acc = fn.call(context, acc, item.data, item, this);
+ }
+
+ // pop cursor
+ releaseCursor(this);
+
+ return acc;
+ };
+
+ List$6.prototype.reduceRight = function(fn, initialValue, context) {
+ var item;
+
+ if (context === undefined) {
+ context = this;
+ }
+
+ // push cursor
+ var cursor = allocateCursor(this, this.tail, null);
+ var acc = initialValue;
+
+ while (cursor.prev !== null) {
+ item = cursor.prev;
+ cursor.prev = item.prev;
+
+ acc = fn.call(context, acc, item.data, item, this);
+ }
+
+ // pop cursor
+ releaseCursor(this);
+
+ return acc;
+ };
+
+ List$6.prototype.nextUntil = function(start, fn, context) {
+ if (start === null) {
+ return;
+ }
+
+ var item;
+
+ if (context === undefined) {
+ context = this;
+ }
+
+ // push cursor
+ var cursor = allocateCursor(this, null, start);
+
+ while (cursor.next !== null) {
+ item = cursor.next;
+ cursor.next = item.next;
+
+ if (fn.call(context, item.data, item, this)) {
+ break;
+ }
+ }
+
+ // pop cursor
+ releaseCursor(this);
+ };
+
+ List$6.prototype.prevUntil = function(start, fn, context) {
+ if (start === null) {
+ return;
+ }
+
+ var item;
+
+ if (context === undefined) {
+ context = this;
+ }
+
+ // push cursor
+ var cursor = allocateCursor(this, start, null);
+
+ while (cursor.prev !== null) {
+ item = cursor.prev;
+ cursor.prev = item.prev;
+
+ if (fn.call(context, item.data, item, this)) {
+ break;
+ }
+ }
+
+ // pop cursor
+ releaseCursor(this);
+ };
+
+ List$6.prototype.some = function(fn, context) {
+ var cursor = this.head;
+
+ if (context === undefined) {
+ context = this;
+ }
+
+ while (cursor !== null) {
+ if (fn.call(context, cursor.data, cursor, this)) {
+ return true;
+ }
+
+ cursor = cursor.next;
+ }
+
+ return false;
+ };
+
+ List$6.prototype.map = function(fn, context) {
+ var result = new List$6();
+ var cursor = this.head;
+
+ if (context === undefined) {
+ context = this;
+ }
+
+ while (cursor !== null) {
+ result.appendData(fn.call(context, cursor.data, cursor, this));
+ cursor = cursor.next;
+ }
+
+ return result;
+ };
+
+ List$6.prototype.filter = function(fn, context) {
+ var result = new List$6();
+ var cursor = this.head;
+
+ if (context === undefined) {
+ context = this;
+ }
+
+ while (cursor !== null) {
+ if (fn.call(context, cursor.data, cursor, this)) {
+ result.appendData(cursor.data);
+ }
+ cursor = cursor.next;
+ }
+
+ return result;
+ };
+
+ List$6.prototype.clear = function() {
+ this.head = null;
+ this.tail = null;
+ };
+
+ List$6.prototype.copy = function() {
+ var result = new List$6();
+ var cursor = this.head;
+
+ while (cursor !== null) {
+ result.insert(createItem(cursor.data));
+ cursor = cursor.next;
+ }
+
+ return result;
+ };
+
+ List$6.prototype.prepend = function(item) {
+ // head
+ // ^
+ // item
+ this.updateCursors(null, item, this.head, item);
+
+ // insert to the beginning of the list
+ if (this.head !== null) {
+ // new item <- first item
+ this.head.prev = item;
+
+ // new item -> first item
+ item.next = this.head;
+ } else {
+ // if list has no head, then it also has no tail
+ // in this case tail points to the new item
+ this.tail = item;
+ }
+
+ // head always points to new item
+ this.head = item;
+
+ return this;
+ };
+
+ List$6.prototype.prependData = function(data) {
+ return this.prepend(createItem(data));
+ };
+
+ List$6.prototype.append = function(item) {
+ return this.insert(item);
+ };
+
+ List$6.prototype.appendData = function(data) {
+ return this.insert(createItem(data));
+ };
+
+ List$6.prototype.insert = function(item, before) {
+ if (before !== undefined && before !== null) {
+ // prev before
+ // ^
+ // item
+ this.updateCursors(before.prev, item, before, item);
+
+ if (before.prev === null) {
+ // insert to the beginning of list
+ if (this.head !== before) {
+ throw new Error('before doesn\'t belong to list');
+ }
+
+ // since head points to before therefore list doesn't empty
+ // no need to check tail
+ this.head = item;
+ before.prev = item;
+ item.next = before;
+
+ this.updateCursors(null, item);
+ } else {
+
+ // insert between two items
+ before.prev.next = item;
+ item.prev = before.prev;
+
+ before.prev = item;
+ item.next = before;
+ }
+ } else {
+ // tail
+ // ^
+ // item
+ this.updateCursors(this.tail, item, null, item);
+
+ // insert to the ending of the list
+ if (this.tail !== null) {
+ // last item -> new item
+ this.tail.next = item;
+
+ // last item <- new item
+ item.prev = this.tail;
+ } else {
+ // if list has no tail, then it also has no head
+ // in this case head points to new item
+ this.head = item;
+ }
+
+ // tail always points to new item
+ this.tail = item;
+ }
+
+ return this;
+ };
+
+ List$6.prototype.insertData = function(data, before) {
+ return this.insert(createItem(data), before);
+ };
+
+ List$6.prototype.remove = function(item) {
+ // item
+ // ^
+ // prev next
+ this.updateCursors(item, item.prev, item, item.next);
+
+ if (item.prev !== null) {
+ item.prev.next = item.next;
+ } else {
+ if (this.head !== item) {
+ throw new Error('item doesn\'t belong to list');
+ }
+
+ this.head = item.next;
+ }
+
+ if (item.next !== null) {
+ item.next.prev = item.prev;
+ } else {
+ if (this.tail !== item) {
+ throw new Error('item doesn\'t belong to list');
+ }
+
+ this.tail = item.prev;
+ }
+
+ item.prev = null;
+ item.next = null;
+
+ return item;
+ };
+
+ List$6.prototype.push = function(data) {
+ this.insert(createItem(data));
+ };
+
+ List$6.prototype.pop = function() {
+ if (this.tail !== null) {
+ return this.remove(this.tail);
+ }
+ };
+
+ List$6.prototype.unshift = function(data) {
+ this.prepend(createItem(data));
+ };
+
+ List$6.prototype.shift = function() {
+ if (this.head !== null) {
+ return this.remove(this.head);
+ }
+ };
+
+ List$6.prototype.prependList = function(list) {
+ return this.insertList(list, this.head);
+ };
+
+ List$6.prototype.appendList = function(list) {
+ return this.insertList(list);
+ };
+
+ List$6.prototype.insertList = function(list, before) {
+ // ignore empty lists
+ if (list.head === null) {
+ return this;
+ }
+
+ if (before !== undefined && before !== null) {
+ this.updateCursors(before.prev, list.tail, before, list.head);
+
+ // insert in the middle of dist list
+ if (before.prev !== null) {
+ // before.prev <-> list.head
+ before.prev.next = list.head;
+ list.head.prev = before.prev;
+ } else {
+ this.head = list.head;
+ }
+
+ before.prev = list.tail;
+ list.tail.next = before;
+ } else {
+ this.updateCursors(this.tail, list.tail, null, list.head);
+
+ // insert to end of the list
+ if (this.tail !== null) {
+ // if destination list has a tail, then it also has a head,
+ // but head doesn't change
+
+ // dest tail -> source head
+ this.tail.next = list.head;
+
+ // dest tail <- source head
+ list.head.prev = this.tail;
+ } else {
+ // if list has no a tail, then it also has no a head
+ // in this case points head to new item
+ this.head = list.head;
+ }
+
+ // tail always start point to new item
+ this.tail = list.tail;
+ }
+
+ list.head = null;
+ list.tail = null;
+
+ return this;
+ };
+
+ List$6.prototype.replace = function(oldItem, newItemOrList) {
+ if ('head' in newItemOrList) {
+ this.insertList(newItemOrList, oldItem);
+ } else {
+ this.insert(newItemOrList, oldItem);
+ }
+
+ this.remove(oldItem);
+ };
+
+ var List_1 = List$6;
+
+ var createCustomError$3 = function createCustomError(name, message) {
+ // use Object.create(), because some VMs prevent setting line/column otherwise
+ // (iOS Safari 10 even throws an exception)
+ var error = Object.create(SyntaxError.prototype);
+ var errorStack = new Error();
+
+ error.name = name;
+ error.message = message;
+
+ Object.defineProperty(error, 'stack', {
+ get: function() {
+ return (errorStack.stack || '').replace(/^(.+\n){1,3}/, name + ': ' + message + '\n');
+ }
+ });
+
+ return error;
+ };
+
+ var createCustomError$2 = createCustomError$3;
+ var MAX_LINE_LENGTH = 100;
+ var OFFSET_CORRECTION = 60;
+ var TAB_REPLACEMENT = ' ';
+
+ function sourceFragment(error, extraLines) {
+ function processLines(start, end) {
+ return lines.slice(start, end).map(function(line, idx) {
+ var num = String(start + idx + 1);
+
+ while (num.length < maxNumLength) {
+ num = ' ' + num;
+ }
+
+ return num + ' |' + line;
+ }).join('\n');
+ }
+
+ var lines = error.source.split(/\r\n?|\n|\f/);
+ var line = error.line;
+ var column = error.column;
+ var startLine = Math.max(1, line - extraLines) - 1;
+ var endLine = Math.min(line + extraLines, lines.length + 1);
+ var maxNumLength = Math.max(4, String(endLine).length) + 1;
+ var cutLeft = 0;
+
+ // column correction according to replaced tab before column
+ column += (TAB_REPLACEMENT.length - 1) * (lines[line - 1].substr(0, column - 1).match(/\t/g) || []).length;
+
+ if (column > MAX_LINE_LENGTH) {
+ cutLeft = column - OFFSET_CORRECTION + 3;
+ column = OFFSET_CORRECTION - 2;
+ }
+
+ for (var i = startLine; i <= endLine; i++) {
+ if (i >= 0 && i < lines.length) {
+ lines[i] = lines[i].replace(/\t/g, TAB_REPLACEMENT);
+ lines[i] =
+ (cutLeft > 0 && lines[i].length > cutLeft ? '\u2026' : '') +
+ lines[i].substr(cutLeft, MAX_LINE_LENGTH - 2) +
+ (lines[i].length > cutLeft + MAX_LINE_LENGTH - 1 ? '\u2026' : '');
+ }
+ }
+
+ return [
+ processLines(startLine, line),
+ new Array(column + maxNumLength + 2).join('-') + '^',
+ processLines(line, endLine)
+ ].filter(Boolean).join('\n');
+ }
+
+ var SyntaxError$4 = function(message, source, offset, line, column) {
+ var error = createCustomError$2('SyntaxError', message);
+
+ error.source = source;
+ error.offset = offset;
+ error.line = line;
+ error.column = column;
+
+ error.sourceFragment = function(extraLines) {
+ return sourceFragment(error, isNaN(extraLines) ? 0 : extraLines);
+ };
+ Object.defineProperty(error, 'formattedMessage', {
+ get: function() {
+ return (
+ 'Parse error: ' + error.message + '\n' +
+ sourceFragment(error, 2)
+ );
+ }
+ });
+
+ // for backward capability
+ error.parseError = {
+ offset: offset,
+ line: line,
+ column: column
+ };
+
+ return error;
+ };
+
+ var _SyntaxError$1 = SyntaxError$4;
+
+ // CSS Syntax Module Level 3
+ // https://www.w3.org/TR/css-syntax-3/
+ var TYPE$H = {
+ EOF: 0, //
+ Ident: 1, //
+ Function: 2, //
+ AtKeyword: 3, //
+ Hash: 4, //
+ String: 5, //
+ BadString: 6, //
+ Url: 7, //
+ BadUrl: 8, //
+ Delim: 9, //
+ Number: 10, //
+ Percentage: 11, //
+ Dimension: 12, //
+ WhiteSpace: 13, //
+ CDO: 14, //
+ CDC: 15, //
+ Colon: 16, // :
+ Semicolon: 17, // ;
+ Comma: 18, // ,
+ LeftSquareBracket: 19, // <[-token>
+ RightSquareBracket: 20, // <]-token>
+ LeftParenthesis: 21, // <(-token>
+ RightParenthesis: 22, // <)-token>
+ LeftCurlyBracket: 23, // <{-token>
+ RightCurlyBracket: 24, // <}-token>
+ Comment: 25
+ };
+
+ var NAME$3 = Object.keys(TYPE$H).reduce(function(result, key) {
+ result[TYPE$H[key]] = key;
+ return result;
+ }, {});
+
+ var _const = {
+ TYPE: TYPE$H,
+ NAME: NAME$3
+ };
+
+ var EOF$1 = 0;
+
+ // https://drafts.csswg.org/css-syntax-3/
+ // § 4.2. Definitions
+
+ // digit
+ // A code point between U+0030 DIGIT ZERO (0) and U+0039 DIGIT NINE (9).
+ function isDigit$5(code) {
+ return code >= 0x0030 && code <= 0x0039;
+ }
+
+ // hex digit
+ // A digit, or a code point between U+0041 LATIN CAPITAL LETTER A (A) and U+0046 LATIN CAPITAL LETTER F (F),
+ // or a code point between U+0061 LATIN SMALL LETTER A (a) and U+0066 LATIN SMALL LETTER F (f).
+ function isHexDigit$4(code) {
+ return (
+ isDigit$5(code) || // 0 .. 9
+ (code >= 0x0041 && code <= 0x0046) || // A .. F
+ (code >= 0x0061 && code <= 0x0066) // a .. f
+ );
+ }
+
+ // uppercase letter
+ // A code point between U+0041 LATIN CAPITAL LETTER A (A) and U+005A LATIN CAPITAL LETTER Z (Z).
+ function isUppercaseLetter$1(code) {
+ return code >= 0x0041 && code <= 0x005A;
+ }
+
+ // lowercase letter
+ // A code point between U+0061 LATIN SMALL LETTER A (a) and U+007A LATIN SMALL LETTER Z (z).
+ function isLowercaseLetter(code) {
+ return code >= 0x0061 && code <= 0x007A;
+ }
+
+ // letter
+ // An uppercase letter or a lowercase letter.
+ function isLetter(code) {
+ return isUppercaseLetter$1(code) || isLowercaseLetter(code);
+ }
+
+ // non-ASCII code point
+ // A code point with a value equal to or greater than U+0080 .
+ function isNonAscii(code) {
+ return code >= 0x0080;
+ }
+
+ // name-start code point
+ // A letter, a non-ASCII code point, or U+005F LOW LINE (_).
+ function isNameStart(code) {
+ return isLetter(code) || isNonAscii(code) || code === 0x005F;
+ }
+
+ // name code point
+ // A name-start code point, a digit, or U+002D HYPHEN-MINUS (-).
+ function isName$2(code) {
+ return isNameStart(code) || isDigit$5(code) || code === 0x002D;
+ }
+
+ // non-printable code point
+ // A code point between U+0000 NULL and U+0008 BACKSPACE, or U+000B LINE TABULATION,
+ // or a code point between U+000E SHIFT OUT and U+001F INFORMATION SEPARATOR ONE, or U+007F DELETE.
+ function isNonPrintable(code) {
+ return (
+ (code >= 0x0000 && code <= 0x0008) ||
+ (code === 0x000B) ||
+ (code >= 0x000E && code <= 0x001F) ||
+ (code === 0x007F)
+ );
+ }
+
+ // newline
+ // U+000A LINE FEED. Note that U+000D CARRIAGE RETURN and U+000C FORM FEED are not included in this definition,
+ // as they are converted to U+000A LINE FEED during preprocessing.
+ // TODO: we doesn't do a preprocessing, so check a code point for U+000D CARRIAGE RETURN and U+000C FORM FEED
+ function isNewline$1(code) {
+ return code === 0x000A || code === 0x000D || code === 0x000C;
+ }
+
+ // whitespace
+ // A newline, U+0009 CHARACTER TABULATION, or U+0020 SPACE.
+ function isWhiteSpace$2(code) {
+ return isNewline$1(code) || code === 0x0020 || code === 0x0009;
+ }
+
+ // § 4.3.8. Check if two code points are a valid escape
+ function isValidEscape$2(first, second) {
+ // If the first code point is not U+005C REVERSE SOLIDUS (\), return false.
+ if (first !== 0x005C) {
+ return false;
+ }
+
+ // Otherwise, if the second code point is a newline or EOF, return false.
+ if (isNewline$1(second) || second === EOF$1) {
+ return false;
+ }
+
+ // Otherwise, return true.
+ return true;
+ }
+
+ // § 4.3.9. Check if three code points would start an identifier
+ function isIdentifierStart$2(first, second, third) {
+ // Look at the first code point:
+
+ // U+002D HYPHEN-MINUS
+ if (first === 0x002D) {
+ // If the second code point is a name-start code point or a U+002D HYPHEN-MINUS,
+ // or the second and third code points are a valid escape, return true. Otherwise, return false.
+ return (
+ isNameStart(second) ||
+ second === 0x002D ||
+ isValidEscape$2(second, third)
+ );
+ }
+
+ // name-start code point
+ if (isNameStart(first)) {
+ // Return true.
+ return true;
+ }
+
+ // U+005C REVERSE SOLIDUS (\)
+ if (first === 0x005C) {
+ // If the first and second code points are a valid escape, return true. Otherwise, return false.
+ return isValidEscape$2(first, second);
+ }
+
+ // anything else
+ // Return false.
+ return false;
+ }
+
+ // § 4.3.10. Check if three code points would start a number
+ function isNumberStart$1(first, second, third) {
+ // Look at the first code point:
+
+ // U+002B PLUS SIGN (+)
+ // U+002D HYPHEN-MINUS (-)
+ if (first === 0x002B || first === 0x002D) {
+ // If the second code point is a digit, return true.
+ if (isDigit$5(second)) {
+ return 2;
+ }
+
+ // Otherwise, if the second code point is a U+002E FULL STOP (.)
+ // and the third code point is a digit, return true.
+ // Otherwise, return false.
+ return second === 0x002E && isDigit$5(third) ? 3 : 0;
+ }
+
+ // U+002E FULL STOP (.)
+ if (first === 0x002E) {
+ // If the second code point is a digit, return true. Otherwise, return false.
+ return isDigit$5(second) ? 2 : 0;
+ }
+
+ // digit
+ if (isDigit$5(first)) {
+ // Return true.
+ return 1;
+ }
+
+ // anything else
+ // Return false.
+ return 0;
+ }
+
+ //
+ // Misc
+ //
+
+ // detect BOM (https://en.wikipedia.org/wiki/Byte_order_mark)
+ function isBOM$2(code) {
+ // UTF-16BE
+ if (code === 0xFEFF) {
+ return 1;
+ }
+
+ // UTF-16LE
+ if (code === 0xFFFE) {
+ return 1;
+ }
+
+ return 0;
+ }
+
+ // Fast code category
+ //
+ // https://drafts.csswg.org/css-syntax/#tokenizer-definitions
+ // > non-ASCII code point
+ // > A code point with a value equal to or greater than U+0080
+ // > name-start code point
+ // > A letter, a non-ASCII code point, or U+005F LOW LINE (_).
+ // > name code point
+ // > A name-start code point, a digit, or U+002D HYPHEN-MINUS (-)
+ // That means only ASCII code points has a special meaning and we define a maps for 0..127 codes only
+ var CATEGORY = new Array(0x80);
+ charCodeCategory$1.Eof = 0x80;
+ charCodeCategory$1.WhiteSpace = 0x82;
+ charCodeCategory$1.Digit = 0x83;
+ charCodeCategory$1.NameStart = 0x84;
+ charCodeCategory$1.NonPrintable = 0x85;
+
+ for (var i = 0; i < CATEGORY.length; i++) {
+ switch (true) {
+ case isWhiteSpace$2(i):
+ CATEGORY[i] = charCodeCategory$1.WhiteSpace;
+ break;
+
+ case isDigit$5(i):
+ CATEGORY[i] = charCodeCategory$1.Digit;
+ break;
+
+ case isNameStart(i):
+ CATEGORY[i] = charCodeCategory$1.NameStart;
+ break;
+
+ case isNonPrintable(i):
+ CATEGORY[i] = charCodeCategory$1.NonPrintable;
+ break;
+
+ default:
+ CATEGORY[i] = i || charCodeCategory$1.Eof;
+ }
+ }
+
+ function charCodeCategory$1(code) {
+ return code < 0x80 ? CATEGORY[code] : charCodeCategory$1.NameStart;
+ }
+ var charCodeDefinitions$1 = {
+ isDigit: isDigit$5,
+ isHexDigit: isHexDigit$4,
+ isUppercaseLetter: isUppercaseLetter$1,
+ isLowercaseLetter: isLowercaseLetter,
+ isLetter: isLetter,
+ isNonAscii: isNonAscii,
+ isNameStart: isNameStart,
+ isName: isName$2,
+ isNonPrintable: isNonPrintable,
+ isNewline: isNewline$1,
+ isWhiteSpace: isWhiteSpace$2,
+ isValidEscape: isValidEscape$2,
+ isIdentifierStart: isIdentifierStart$2,
+ isNumberStart: isNumberStart$1,
+
+ isBOM: isBOM$2,
+ charCodeCategory: charCodeCategory$1
+ };
+
+ var charCodeDef = charCodeDefinitions$1;
+ var isDigit$4 = charCodeDef.isDigit;
+ var isHexDigit$3 = charCodeDef.isHexDigit;
+ var isUppercaseLetter = charCodeDef.isUppercaseLetter;
+ var isName$1 = charCodeDef.isName;
+ var isWhiteSpace$1 = charCodeDef.isWhiteSpace;
+ var isValidEscape$1 = charCodeDef.isValidEscape;
+
+ function getCharCode(source, offset) {
+ return offset < source.length ? source.charCodeAt(offset) : 0;
+ }
+
+ function getNewlineLength$1(source, offset, code) {
+ if (code === 13 /* \r */ && getCharCode(source, offset + 1) === 10 /* \n */) {
+ return 2;
+ }
+
+ return 1;
+ }
+
+ function cmpChar$5(testStr, offset, referenceCode) {
+ var code = testStr.charCodeAt(offset);
+
+ // code.toLowerCase() for A..Z
+ if (isUppercaseLetter(code)) {
+ code = code | 32;
+ }
+
+ return code === referenceCode;
+ }
+
+ function cmpStr$6(testStr, start, end, referenceStr) {
+ if (end - start !== referenceStr.length) {
+ return false;
+ }
+
+ if (start < 0 || end > testStr.length) {
+ return false;
+ }
+
+ for (var i = start; i < end; i++) {
+ var testCode = testStr.charCodeAt(i);
+ var referenceCode = referenceStr.charCodeAt(i - start);
+
+ // testCode.toLowerCase() for A..Z
+ if (isUppercaseLetter(testCode)) {
+ testCode = testCode | 32;
+ }
+
+ if (testCode !== referenceCode) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ function findWhiteSpaceStart$1(source, offset) {
+ for (; offset >= 0; offset--) {
+ if (!isWhiteSpace$1(source.charCodeAt(offset))) {
+ break;
+ }
+ }
+
+ return offset + 1;
+ }
+
+ function findWhiteSpaceEnd$1(source, offset) {
+ for (; offset < source.length; offset++) {
+ if (!isWhiteSpace$1(source.charCodeAt(offset))) {
+ break;
+ }
+ }
+
+ return offset;
+ }
+
+ function findDecimalNumberEnd(source, offset) {
+ for (; offset < source.length; offset++) {
+ if (!isDigit$4(source.charCodeAt(offset))) {
+ break;
+ }
+ }
+
+ return offset;
+ }
+
+ // § 4.3.7. Consume an escaped code point
+ function consumeEscaped$1(source, offset) {
+ // It assumes that the U+005C REVERSE SOLIDUS (\) has already been consumed and
+ // that the next input code point has already been verified to be part of a valid escape.
+ offset += 2;
+
+ // hex digit
+ if (isHexDigit$3(getCharCode(source, offset - 1))) {
+ // Consume as many hex digits as possible, but no more than 5.
+ // Note that this means 1-6 hex digits have been consumed in total.
+ for (var maxOffset = Math.min(source.length, offset + 5); offset < maxOffset; offset++) {
+ if (!isHexDigit$3(getCharCode(source, offset))) {
+ break;
+ }
+ }
+
+ // If the next input code point is whitespace, consume it as well.
+ var code = getCharCode(source, offset);
+ if (isWhiteSpace$1(code)) {
+ offset += getNewlineLength$1(source, offset, code);
+ }
+ }
+
+ return offset;
+ }
+
+ // §4.3.11. Consume a name
+ // Note: This algorithm does not do the verification of the first few code points that are necessary
+ // to ensure the returned code points would constitute an . If that is the intended use,
+ // ensure that the stream starts with an identifier before calling this algorithm.
+ function consumeName$1(source, offset) {
+ // Let result initially be an empty string.
+ // Repeatedly consume the next input code point from the stream:
+ for (; offset < source.length; offset++) {
+ var code = source.charCodeAt(offset);
+
+ // name code point
+ if (isName$1(code)) {
+ // Append the code point to result.
+ continue;
+ }
+
+ // the stream starts with a valid escape
+ if (isValidEscape$1(code, getCharCode(source, offset + 1))) {
+ // Consume an escaped code point. Append the returned code point to result.
+ offset = consumeEscaped$1(source, offset) - 1;
+ continue;
+ }
+
+ // anything else
+ // Reconsume the current input code point. Return result.
+ break;
+ }
+
+ return offset;
+ }
+
+ // §4.3.12. Consume a number
+ function consumeNumber$5(source, offset) {
+ var code = source.charCodeAt(offset);
+
+ // 2. If the next input code point is U+002B PLUS SIGN (+) or U+002D HYPHEN-MINUS (-),
+ // consume it and append it to repr.
+ if (code === 0x002B || code === 0x002D) {
+ code = source.charCodeAt(offset += 1);
+ }
+
+ // 3. While the next input code point is a digit, consume it and append it to repr.
+ if (isDigit$4(code)) {
+ offset = findDecimalNumberEnd(source, offset + 1);
+ code = source.charCodeAt(offset);
+ }
+
+ // 4. If the next 2 input code points are U+002E FULL STOP (.) followed by a digit, then:
+ if (code === 0x002E && isDigit$4(source.charCodeAt(offset + 1))) {
+ // 4.1 Consume them.
+ // 4.2 Append them to repr.
+ code = source.charCodeAt(offset += 2);
+
+ // 4.3 Set type to "number".
+ // TODO
+
+ // 4.4 While the next input code point is a digit, consume it and append it to repr.
+
+ offset = findDecimalNumberEnd(source, offset);
+ }
+
+ // 5. If the next 2 or 3 input code points are U+0045 LATIN CAPITAL LETTER E (E)
+ // or U+0065 LATIN SMALL LETTER E (e), ... , followed by a digit, then:
+ if (cmpChar$5(source, offset, 101 /* e */)) {
+ var sign = 0;
+ code = source.charCodeAt(offset + 1);
+
+ // ... optionally followed by U+002D HYPHEN-MINUS (-) or U+002B PLUS SIGN (+) ...
+ if (code === 0x002D || code === 0x002B) {
+ sign = 1;
+ code = source.charCodeAt(offset + 2);
+ }
+
+ // ... followed by a digit
+ if (isDigit$4(code)) {
+ // 5.1 Consume them.
+ // 5.2 Append them to repr.
+
+ // 5.3 Set type to "number".
+ // TODO
+
+ // 5.4 While the next input code point is a digit, consume it and append it to repr.
+ offset = findDecimalNumberEnd(source, offset + 1 + sign + 1);
+ }
+ }
+
+ return offset;
+ }
+
+ // § 4.3.14. Consume the remnants of a bad url
+ // ... its sole use is to consume enough of the input stream to reach a recovery point
+ // where normal tokenizing can resume.
+ function consumeBadUrlRemnants$1(source, offset) {
+ // Repeatedly consume the next input code point from the stream:
+ for (; offset < source.length; offset++) {
+ var code = source.charCodeAt(offset);
+
+ // U+0029 RIGHT PARENTHESIS ())
+ // EOF
+ if (code === 0x0029) {
+ // Return.
+ offset++;
+ break;
+ }
+
+ if (isValidEscape$1(code, getCharCode(source, offset + 1))) {
+ // Consume an escaped code point.
+ // Note: This allows an escaped right parenthesis ("\)") to be encountered
+ // without ending the . This is otherwise identical to
+ // the "anything else" clause.
+ offset = consumeEscaped$1(source, offset);
+ }
+ }
+
+ return offset;
+ }
+
+ var utils$2 = {
+ consumeEscaped: consumeEscaped$1,
+ consumeName: consumeName$1,
+ consumeNumber: consumeNumber$5,
+ consumeBadUrlRemnants: consumeBadUrlRemnants$1,
+
+ cmpChar: cmpChar$5,
+ cmpStr: cmpStr$6,
+
+ getNewlineLength: getNewlineLength$1,
+ findWhiteSpaceStart: findWhiteSpaceStart$1,
+ findWhiteSpaceEnd: findWhiteSpaceEnd$1
+ };
+
+ var constants$2 = _const;
+ var TYPE$G = constants$2.TYPE;
+ var NAME$2 = constants$2.NAME;
+
+ var utils$1 = utils$2;
+ var cmpStr$5 = utils$1.cmpStr;
+
+ var EOF = TYPE$G.EOF;
+ var WHITESPACE$c = TYPE$G.WhiteSpace;
+ var COMMENT$a = TYPE$G.Comment;
+
+ var OFFSET_MASK$1 = 0x00FFFFFF;
+ var TYPE_SHIFT$1 = 24;
+
+ var TokenStream$4 = function() {
+ this.offsetAndType = null;
+ this.balance = null;
+
+ this.reset();
+ };
+
+ TokenStream$4.prototype = {
+ reset: function() {
+ this.eof = false;
+ this.tokenIndex = -1;
+ this.tokenType = 0;
+ this.tokenStart = this.firstCharOffset;
+ this.tokenEnd = this.firstCharOffset;
+ },
+
+ lookupType: function(offset) {
+ offset += this.tokenIndex;
+
+ if (offset < this.tokenCount) {
+ return this.offsetAndType[offset] >> TYPE_SHIFT$1;
+ }
+
+ return EOF;
+ },
+ lookupOffset: function(offset) {
+ offset += this.tokenIndex;
+
+ if (offset < this.tokenCount) {
+ return this.offsetAndType[offset - 1] & OFFSET_MASK$1;
+ }
+
+ return this.source.length;
+ },
+ lookupValue: function(offset, referenceStr) {
+ offset += this.tokenIndex;
+
+ if (offset < this.tokenCount) {
+ return cmpStr$5(
+ this.source,
+ this.offsetAndType[offset - 1] & OFFSET_MASK$1,
+ this.offsetAndType[offset] & OFFSET_MASK$1,
+ referenceStr
+ );
+ }
+
+ return false;
+ },
+ getTokenStart: function(tokenIndex) {
+ if (tokenIndex === this.tokenIndex) {
+ return this.tokenStart;
+ }
+
+ if (tokenIndex > 0) {
+ return tokenIndex < this.tokenCount
+ ? this.offsetAndType[tokenIndex - 1] & OFFSET_MASK$1
+ : this.offsetAndType[this.tokenCount] & OFFSET_MASK$1;
+ }
+
+ return this.firstCharOffset;
+ },
+
+ // TODO: -> skipUntilBalanced
+ getRawLength: function(startToken, mode) {
+ var cursor = startToken;
+ var balanceEnd;
+ var offset = this.offsetAndType[Math.max(cursor - 1, 0)] & OFFSET_MASK$1;
+ var type;
+
+ loop:
+ for (; cursor < this.tokenCount; cursor++) {
+ balanceEnd = this.balance[cursor];
+
+ // stop scanning on balance edge that points to offset before start token
+ if (balanceEnd < startToken) {
+ break loop;
+ }
+
+ type = this.offsetAndType[cursor] >> TYPE_SHIFT$1;
+
+ // check token is stop type
+ switch (mode(type, this.source, offset)) {
+ case 1:
+ break loop;
+
+ case 2:
+ cursor++;
+ break loop;
+
+ default:
+ // fast forward to the end of balanced block
+ if (this.balance[balanceEnd] === cursor) {
+ cursor = balanceEnd;
+ }
+
+ offset = this.offsetAndType[cursor] & OFFSET_MASK$1;
+ }
+ }
+
+ return cursor - this.tokenIndex;
+ },
+ isBalanceEdge: function(pos) {
+ return this.balance[this.tokenIndex] < pos;
+ },
+ isDelim: function(code, offset) {
+ if (offset) {
+ return (
+ this.lookupType(offset) === TYPE$G.Delim &&
+ this.source.charCodeAt(this.lookupOffset(offset)) === code
+ );
+ }
+
+ return (
+ this.tokenType === TYPE$G.Delim &&
+ this.source.charCodeAt(this.tokenStart) === code
+ );
+ },
+
+ getTokenValue: function() {
+ return this.source.substring(this.tokenStart, this.tokenEnd);
+ },
+ getTokenLength: function() {
+ return this.tokenEnd - this.tokenStart;
+ },
+ substrToCursor: function(start) {
+ return this.source.substring(start, this.tokenStart);
+ },
+
+ skipWS: function() {
+ for (var i = this.tokenIndex, skipTokenCount = 0; i < this.tokenCount; i++, skipTokenCount++) {
+ if ((this.offsetAndType[i] >> TYPE_SHIFT$1) !== WHITESPACE$c) {
+ break;
+ }
+ }
+
+ if (skipTokenCount > 0) {
+ this.skip(skipTokenCount);
+ }
+ },
+ skipSC: function() {
+ while (this.tokenType === WHITESPACE$c || this.tokenType === COMMENT$a) {
+ this.next();
+ }
+ },
+ skip: function(tokenCount) {
+ var next = this.tokenIndex + tokenCount;
+
+ if (next < this.tokenCount) {
+ this.tokenIndex = next;
+ this.tokenStart = this.offsetAndType[next - 1] & OFFSET_MASK$1;
+ next = this.offsetAndType[next];
+ this.tokenType = next >> TYPE_SHIFT$1;
+ this.tokenEnd = next & OFFSET_MASK$1;
+ } else {
+ this.tokenIndex = this.tokenCount;
+ this.next();
+ }
+ },
+ next: function() {
+ var next = this.tokenIndex + 1;
+
+ if (next < this.tokenCount) {
+ this.tokenIndex = next;
+ this.tokenStart = this.tokenEnd;
+ next = this.offsetAndType[next];
+ this.tokenType = next >> TYPE_SHIFT$1;
+ this.tokenEnd = next & OFFSET_MASK$1;
+ } else {
+ this.tokenIndex = this.tokenCount;
+ this.eof = true;
+ this.tokenType = EOF;
+ this.tokenStart = this.tokenEnd = this.source.length;
+ }
+ },
+
+ forEachToken(fn) {
+ for (var i = 0, offset = this.firstCharOffset; i < this.tokenCount; i++) {
+ var start = offset;
+ var item = this.offsetAndType[i];
+ var end = item & OFFSET_MASK$1;
+ var type = item >> TYPE_SHIFT$1;
+
+ offset = end;
+
+ fn(type, start, end, i);
+ }
+ },
+
+ dump() {
+ var tokens = new Array(this.tokenCount);
+
+ this.forEachToken((type, start, end, index) => {
+ tokens[index] = {
+ idx: index,
+ type: NAME$2[type],
+ chunk: this.source.substring(start, end),
+ balance: this.balance[index]
+ };
+ });
+
+ return tokens;
+ }
+ };
+
+ var TokenStream_1 = TokenStream$4;
+
+ function noop$3(value) {
+ return value;
+ }
+
+ function generateMultiplier(multiplier) {
+ if (multiplier.min === 0 && multiplier.max === 0) {
+ return '*';
+ }
+
+ if (multiplier.min === 0 && multiplier.max === 1) {
+ return '?';
+ }
+
+ if (multiplier.min === 1 && multiplier.max === 0) {
+ return multiplier.comma ? '#' : '+';
+ }
+
+ if (multiplier.min === 1 && multiplier.max === 1) {
+ return '';
+ }
+
+ return (
+ (multiplier.comma ? '#' : '') +
+ (multiplier.min === multiplier.max
+ ? '{' + multiplier.min + '}'
+ : '{' + multiplier.min + ',' + (multiplier.max !== 0 ? multiplier.max : '') + '}'
+ )
+ );
+ }
+
+ function generateTypeOpts(node) {
+ switch (node.type) {
+ case 'Range':
+ return (
+ ' [' +
+ (node.min === null ? '-∞' : node.min) +
+ ',' +
+ (node.max === null ? '∞' : node.max) +
+ ']'
+ );
+
+ default:
+ throw new Error('Unknown node type `' + node.type + '`');
+ }
+ }
+
+ function generateSequence(node, decorate, forceBraces, compact) {
+ var combinator = node.combinator === ' ' || compact ? node.combinator : ' ' + node.combinator + ' ';
+ var result = node.terms.map(function(term) {
+ return generate$2(term, decorate, forceBraces, compact);
+ }).join(combinator);
+
+ if (node.explicit || forceBraces) {
+ result = (compact || result[0] === ',' ? '[' : '[ ') + result + (compact ? ']' : ' ]');
+ }
+
+ return result;
+ }
+
+ function generate$2(node, decorate, forceBraces, compact) {
+ var result;
+
+ switch (node.type) {
+ case 'Group':
+ result =
+ generateSequence(node, decorate, forceBraces, compact) +
+ (node.disallowEmpty ? '!' : '');
+ break;
+
+ case 'Multiplier':
+ // return since node is a composition
+ return (
+ generate$2(node.term, decorate, forceBraces, compact) +
+ decorate(generateMultiplier(node), node)
+ );
+
+ case 'Type':
+ result = '<' + node.name + (node.opts ? decorate(generateTypeOpts(node.opts), node.opts) : '') + '>';
+ break;
+
+ case 'Property':
+ result = '<\'' + node.name + '\'>';
+ break;
+
+ case 'Keyword':
+ result = node.name;
+ break;
+
+ case 'AtKeyword':
+ result = '@' + node.name;
+ break;
+
+ case 'Function':
+ result = node.name + '(';
+ break;
+
+ case 'String':
+ case 'Token':
+ result = node.value;
+ break;
+
+ case 'Comma':
+ result = ',';
+ break;
+
+ default:
+ throw new Error('Unknown node type `' + node.type + '`');
+ }
+
+ return decorate(result, node);
+ }
+
+ var generate_1 = function(node, options) {
+ var decorate = noop$3;
+ var forceBraces = false;
+ var compact = false;
+
+ if (typeof options === 'function') {
+ decorate = options;
+ } else if (options) {
+ forceBraces = Boolean(options.forceBraces);
+ compact = Boolean(options.compact);
+ if (typeof options.decorate === 'function') {
+ decorate = options.decorate;
+ }
+ }
+
+ return generate$2(node, decorate, forceBraces, compact);
+ };
+
+ const createCustomError$1 = createCustomError$3;
+ const generate$1 = generate_1;
+ const defaultLoc = { offset: 0, line: 1, column: 1 };
+
+ function locateMismatch(matchResult, node) {
+ const tokens = matchResult.tokens;
+ const longestMatch = matchResult.longestMatch;
+ const mismatchNode = longestMatch < tokens.length ? tokens[longestMatch].node || null : null;
+ const badNode = mismatchNode !== node ? mismatchNode : null;
+ let mismatchOffset = 0;
+ let mismatchLength = 0;
+ let entries = 0;
+ let css = '';
+ let start;
+ let end;
+
+ for (let i = 0; i < tokens.length; i++) {
+ const token = tokens[i].value;
+
+ if (i === longestMatch) {
+ mismatchLength = token.length;
+ mismatchOffset = css.length;
+ }
+
+ if (badNode !== null && tokens[i].node === badNode) {
+ if (i <= longestMatch) {
+ entries++;
+ } else {
+ entries = 0;
+ }
+ }
+
+ css += token;
+ }
+
+ if (longestMatch === tokens.length || entries > 1) { // last
+ start = fromLoc(badNode || node, 'end') || buildLoc(defaultLoc, css);
+ end = buildLoc(start);
+ } else {
+ start = fromLoc(badNode, 'start') ||
+ buildLoc(fromLoc(node, 'start') || defaultLoc, css.slice(0, mismatchOffset));
+ end = fromLoc(badNode, 'end') ||
+ buildLoc(start, css.substr(mismatchOffset, mismatchLength));
+ }
+
+ return {
+ css,
+ mismatchOffset,
+ mismatchLength,
+ start,
+ end
+ };
+ }
+
+ function fromLoc(node, point) {
+ const value = node && node.loc && node.loc[point];
+
+ if (value) {
+ return 'line' in value ? buildLoc(value) : value;
+ }
+
+ return null;
+ }
+
+ function buildLoc({ offset, line, column }, extra) {
+ const loc = {
+ offset,
+ line,
+ column
+ };
+
+ if (extra) {
+ const lines = extra.split(/\n|\r\n?|\f/);
+
+ loc.offset += extra.length;
+ loc.line += lines.length - 1;
+ loc.column = lines.length === 1 ? loc.column + extra.length : lines.pop().length + 1;
+ }
+
+ return loc;
+ }
+
+ const SyntaxReferenceError$1 = function(type, referenceName) {
+ const error = createCustomError$1(
+ 'SyntaxReferenceError',
+ type + (referenceName ? ' `' + referenceName + '`' : '')
+ );
+
+ error.reference = referenceName;
+
+ return error;
+ };
+
+ const SyntaxMatchError$1 = function(message, syntax, node, matchResult) {
+ const error = createCustomError$1('SyntaxMatchError', message);
+ const {
+ css,
+ mismatchOffset,
+ mismatchLength,
+ start,
+ end
+ } = locateMismatch(matchResult, node);
+
+ error.rawMessage = message;
+ error.syntax = syntax ? generate$1(syntax) : '';
+ error.css = css;
+ error.mismatchOffset = mismatchOffset;
+ error.mismatchLength = mismatchLength;
+ error.message = message + '\n' +
+ ' syntax: ' + error.syntax + '\n' +
+ ' value: ' + (css || '') + '\n' +
+ ' --------' + new Array(error.mismatchOffset + 1).join('-') + '^';
+
+ Object.assign(error, start);
+ error.loc = {
+ source: (node && node.loc && node.loc.source) || '',
+ start,
+ end
+ };
+
+ return error;
+ };
+
+ var error = {
+ SyntaxReferenceError: SyntaxReferenceError$1,
+ SyntaxMatchError: SyntaxMatchError$1
+ };
+
+ var hasOwnProperty$7 = Object.prototype.hasOwnProperty;
+ var keywords$1 = Object.create(null);
+ var properties$1 = Object.create(null);
+ var HYPHENMINUS$5 = 45; // '-'.charCodeAt()
+
+ function isCustomProperty$1(str, offset) {
+ offset = offset || 0;
+
+ return str.length - offset >= 2 &&
+ str.charCodeAt(offset) === HYPHENMINUS$5 &&
+ str.charCodeAt(offset + 1) === HYPHENMINUS$5;
+ }
+
+ function getVendorPrefix(str, offset) {
+ offset = offset || 0;
+
+ // verdor prefix should be at least 3 chars length
+ if (str.length - offset >= 3) {
+ // vendor prefix starts with hyper minus following non-hyper minus
+ if (str.charCodeAt(offset) === HYPHENMINUS$5 &&
+ str.charCodeAt(offset + 1) !== HYPHENMINUS$5) {
+ // vendor prefix should contain a hyper minus at the ending
+ var secondDashIndex = str.indexOf('-', offset + 2);
+
+ if (secondDashIndex !== -1) {
+ return str.substring(offset, secondDashIndex + 1);
+ }
+ }
+ }
+
+ return '';
+ }
+
+ function getKeywordDescriptor(keyword) {
+ if (hasOwnProperty$7.call(keywords$1, keyword)) {
+ return keywords$1[keyword];
+ }
+
+ var name = keyword.toLowerCase();
+
+ if (hasOwnProperty$7.call(keywords$1, name)) {
+ return keywords$1[keyword] = keywords$1[name];
+ }
+
+ var custom = isCustomProperty$1(name, 0);
+ var vendor = !custom ? getVendorPrefix(name, 0) : '';
+
+ return keywords$1[keyword] = Object.freeze({
+ basename: name.substr(vendor.length),
+ name: name,
+ vendor: vendor,
+ prefix: vendor,
+ custom: custom
+ });
+ }
+
+ function getPropertyDescriptor(property) {
+ if (hasOwnProperty$7.call(properties$1, property)) {
+ return properties$1[property];
+ }
+
+ var name = property;
+ var hack = property[0];
+
+ if (hack === '/') {
+ hack = property[1] === '/' ? '//' : '/';
+ } else if (hack !== '_' &&
+ hack !== '*' &&
+ hack !== '$' &&
+ hack !== '#' &&
+ hack !== '+' &&
+ hack !== '&') {
+ hack = '';
+ }
+
+ var custom = isCustomProperty$1(name, hack.length);
+
+ // re-use result when possible (the same as for lower case)
+ if (!custom) {
+ name = name.toLowerCase();
+ if (hasOwnProperty$7.call(properties$1, name)) {
+ return properties$1[property] = properties$1[name];
+ }
+ }
+
+ var vendor = !custom ? getVendorPrefix(name, hack.length) : '';
+ var prefix = name.substr(0, hack.length + vendor.length);
+
+ return properties$1[property] = Object.freeze({
+ basename: name.substr(prefix.length),
+ name: name.substr(hack.length),
+ hack: hack,
+ vendor: vendor,
+ prefix: prefix,
+ custom: custom
+ });
+ }
+
+ var names$2 = {
+ keyword: getKeywordDescriptor,
+ property: getPropertyDescriptor,
+ isCustomProperty: isCustomProperty$1,
+ vendorPrefix: getVendorPrefix
+ };
+
+ var MIN_SIZE = 16 * 1024;
+ var SafeUint32Array = typeof Uint32Array !== 'undefined' ? Uint32Array : Array; // fallback on Array when TypedArray is not supported
+
+ var adoptBuffer$2 = function adoptBuffer(buffer, size) {
+ if (buffer === null || buffer.length < size) {
+ return new SafeUint32Array(Math.max(size + 1024, MIN_SIZE));
+ }
+
+ return buffer;
+ };
+
+ var TokenStream$3 = TokenStream_1;
+ var adoptBuffer$1 = adoptBuffer$2;
+
+ var constants$1 = _const;
+ var TYPE$F = constants$1.TYPE;
+
+ var charCodeDefinitions = charCodeDefinitions$1;
+ var isNewline = charCodeDefinitions.isNewline;
+ var isName = charCodeDefinitions.isName;
+ var isValidEscape = charCodeDefinitions.isValidEscape;
+ var isNumberStart = charCodeDefinitions.isNumberStart;
+ var isIdentifierStart$1 = charCodeDefinitions.isIdentifierStart;
+ var charCodeCategory = charCodeDefinitions.charCodeCategory;
+ var isBOM$1 = charCodeDefinitions.isBOM;
+
+ var utils = utils$2;
+ var cmpStr$4 = utils.cmpStr;
+ var getNewlineLength = utils.getNewlineLength;
+ var findWhiteSpaceEnd = utils.findWhiteSpaceEnd;
+ var consumeEscaped = utils.consumeEscaped;
+ var consumeName = utils.consumeName;
+ var consumeNumber$4 = utils.consumeNumber;
+ var consumeBadUrlRemnants = utils.consumeBadUrlRemnants;
+
+ var OFFSET_MASK = 0x00FFFFFF;
+ var TYPE_SHIFT = 24;
+
+ function tokenize$3(source, stream) {
+ function getCharCode(offset) {
+ return offset < sourceLength ? source.charCodeAt(offset) : 0;
+ }
+
+ // § 4.3.3. Consume a numeric token
+ function consumeNumericToken() {
+ // Consume a number and let number be the result.
+ offset = consumeNumber$4(source, offset);
+
+ // If the next 3 input code points would start an identifier, then:
+ if (isIdentifierStart$1(getCharCode(offset), getCharCode(offset + 1), getCharCode(offset + 2))) {
+ // Create a with the same value and type flag as number, and a unit set initially to the empty string.
+ // Consume a name. Set the ’s unit to the returned value.
+ // Return the .
+ type = TYPE$F.Dimension;
+ offset = consumeName(source, offset);
+ return;
+ }
+
+ // Otherwise, if the next input code point is U+0025 PERCENTAGE SIGN (%), consume it.
+ if (getCharCode(offset) === 0x0025) {
+ // Create a with the same value as number, and return it.
+ type = TYPE$F.Percentage;
+ offset++;
+ return;
+ }
+
+ // Otherwise, create a with the same value and type flag as number, and return it.
+ type = TYPE$F.Number;
+ }
+
+ // § 4.3.4. Consume an ident-like token
+ function consumeIdentLikeToken() {
+ const nameStartOffset = offset;
+
+ // Consume a name, and let string be the result.
+ offset = consumeName(source, offset);
+
+ // If string’s value is an ASCII case-insensitive match for "url",
+ // and the next input code point is U+0028 LEFT PARENTHESIS ((), consume it.
+ if (cmpStr$4(source, nameStartOffset, offset, 'url') && getCharCode(offset) === 0x0028) {
+ // While the next two input code points are whitespace, consume the next input code point.
+ offset = findWhiteSpaceEnd(source, offset + 1);
+
+ // If the next one or two input code points are U+0022 QUOTATION MARK ("), U+0027 APOSTROPHE ('),
+ // or whitespace followed by U+0022 QUOTATION MARK (") or U+0027 APOSTROPHE ('),
+ // then create a with its value set to string and return it.
+ if (getCharCode(offset) === 0x0022 ||
+ getCharCode(offset) === 0x0027) {
+ type = TYPE$F.Function;
+ offset = nameStartOffset + 4;
+ return;
+ }
+
+ // Otherwise, consume a url token, and return it.
+ consumeUrlToken();
+ return;
+ }
+
+ // Otherwise, if the next input code point is U+0028 LEFT PARENTHESIS ((), consume it.
+ // Create a with its value set to string and return it.
+ if (getCharCode(offset) === 0x0028) {
+ type = TYPE$F.Function;
+ offset++;
+ return;
+ }
+
+ // Otherwise, create an with its value set to string and return it.
+ type = TYPE$F.Ident;
+ }
+
+ // § 4.3.5. Consume a string token
+ function consumeStringToken(endingCodePoint) {
+ // This algorithm may be called with an ending code point, which denotes the code point
+ // that ends the string. If an ending code point is not specified,
+ // the current input code point is used.
+ if (!endingCodePoint) {
+ endingCodePoint = getCharCode(offset++);
+ }
+
+ // Initially create a with its value set to the empty string.
+ type = TYPE$F.String;
+
+ // Repeatedly consume the next input code point from the stream:
+ for (; offset < source.length; offset++) {
+ var code = source.charCodeAt(offset);
+
+ switch (charCodeCategory(code)) {
+ // ending code point
+ case endingCodePoint:
+ // Return the .
+ offset++;
+ return;
+
+ // EOF
+ case charCodeCategory.Eof:
+ // This is a parse error. Return the .
+ return;
+
+ // newline
+ case charCodeCategory.WhiteSpace:
+ if (isNewline(code)) {
+ // This is a parse error. Reconsume the current input code point,
+ // create a , and return it.
+ offset += getNewlineLength(source, offset, code);
+ type = TYPE$F.BadString;
+ return;
+ }
+ break;
+
+ // U+005C REVERSE SOLIDUS (\)
+ case 0x005C:
+ // If the next input code point is EOF, do nothing.
+ if (offset === source.length - 1) {
+ break;
+ }
+
+ var nextCode = getCharCode(offset + 1);
+
+ // Otherwise, if the next input code point is a newline, consume it.
+ if (isNewline(nextCode)) {
+ offset += getNewlineLength(source, offset + 1, nextCode);
+ } else if (isValidEscape(code, nextCode)) {
+ // Otherwise, (the stream starts with a valid escape) consume
+ // an escaped code point and append the returned code point to
+ // the ’s value.
+ offset = consumeEscaped(source, offset) - 1;
+ }
+ break;
+
+ // anything else
+ // Append the current input code point to the ’s value.
+ }
+ }
+ }
+
+ // § 4.3.6. Consume a url token
+ // Note: This algorithm assumes that the initial "url(" has already been consumed.
+ // This algorithm also assumes that it’s being called to consume an "unquoted" value, like url(foo).
+ // A quoted value, like url("foo"), is parsed as a . Consume an ident-like token
+ // automatically handles this distinction; this algorithm shouldn’t be called directly otherwise.
+ function consumeUrlToken() {
+ // Initially create a with its value set to the empty string.
+ type = TYPE$F.Url;
+
+ // Consume as much whitespace as possible.
+ offset = findWhiteSpaceEnd(source, offset);
+
+ // Repeatedly consume the next input code point from the stream:
+ for (; offset < source.length; offset++) {
+ var code = source.charCodeAt(offset);
+
+ switch (charCodeCategory(code)) {
+ // U+0029 RIGHT PARENTHESIS ())
+ case 0x0029:
+ // Return the .
+ offset++;
+ return;
+
+ // EOF
+ case charCodeCategory.Eof:
+ // This is a parse error. Return the .
+ return;
+
+ // whitespace
+ case charCodeCategory.WhiteSpace:
+ // Consume as much whitespace as possible.
+ offset = findWhiteSpaceEnd(source, offset);
+
+ // If the next input code point is U+0029 RIGHT PARENTHESIS ()) or EOF,
+ // consume it and return the
+ // (if EOF was encountered, this is a parse error);
+ if (getCharCode(offset) === 0x0029 || offset >= source.length) {
+ if (offset < source.length) {
+ offset++;
+ }
+ return;
+ }
+
+ // otherwise, consume the remnants of a bad url, create a ,
+ // and return it.
+ offset = consumeBadUrlRemnants(source, offset);
+ type = TYPE$F.BadUrl;
+ return;
+
+ // U+0022 QUOTATION MARK (")
+ // U+0027 APOSTROPHE (')
+ // U+0028 LEFT PARENTHESIS (()
+ // non-printable code point
+ case 0x0022:
+ case 0x0027:
+ case 0x0028:
+ case charCodeCategory.NonPrintable:
+ // This is a parse error. Consume the remnants of a bad url,
+ // create a , and return it.
+ offset = consumeBadUrlRemnants(source, offset);
+ type = TYPE$F.BadUrl;
+ return;
+
+ // U+005C REVERSE SOLIDUS (\)
+ case 0x005C:
+ // If the stream starts with a valid escape, consume an escaped code point and
+ // append the returned code point to the ’s value.
+ if (isValidEscape(code, getCharCode(offset + 1))) {
+ offset = consumeEscaped(source, offset) - 1;
+ break;
+ }
+
+ // Otherwise, this is a parse error. Consume the remnants of a bad url,
+ // create a , and return it.
+ offset = consumeBadUrlRemnants(source, offset);
+ type = TYPE$F.BadUrl;
+ return;
+
+ // anything else
+ // Append the current input code point to the ’s value.
+ }
+ }
+ }
+
+ if (!stream) {
+ stream = new TokenStream$3();
+ }
+
+ // ensure source is a string
+ source = String(source || '');
+
+ var sourceLength = source.length;
+ var offsetAndType = adoptBuffer$1(stream.offsetAndType, sourceLength + 1); // +1 because of eof-token
+ var balance = adoptBuffer$1(stream.balance, sourceLength + 1);
+ var tokenCount = 0;
+ var start = isBOM$1(getCharCode(0));
+ var offset = start;
+ var balanceCloseType = 0;
+ var balanceStart = 0;
+ var balancePrev = 0;
+
+ // https://drafts.csswg.org/css-syntax-3/#consume-token
+ // § 4.3.1. Consume a token
+ while (offset < sourceLength) {
+ var code = source.charCodeAt(offset);
+ var type = 0;
+
+ balance[tokenCount] = sourceLength;
+
+ switch (charCodeCategory(code)) {
+ // whitespace
+ case charCodeCategory.WhiteSpace:
+ // Consume as much whitespace as possible. Return a .
+ type = TYPE$F.WhiteSpace;
+ offset = findWhiteSpaceEnd(source, offset + 1);
+ break;
+
+ // U+0022 QUOTATION MARK (")
+ case 0x0022:
+ // Consume a string token and return it.
+ consumeStringToken();
+ break;
+
+ // U+0023 NUMBER SIGN (#)
+ case 0x0023:
+ // If the next input code point is a name code point or the next two input code points are a valid escape, then:
+ if (isName(getCharCode(offset + 1)) || isValidEscape(getCharCode(offset + 1), getCharCode(offset + 2))) {
+ // Create a .
+ type = TYPE$F.Hash;
+
+ // If the next 3 input code points would start an identifier, set the ’s type flag to "id".
+ // if (isIdentifierStart(getCharCode(offset + 1), getCharCode(offset + 2), getCharCode(offset + 3))) {
+ // // TODO: set id flag
+ // }
+
+ // Consume a name, and set the ’s value to the returned string.
+ offset = consumeName(source, offset + 1);
+
+ // Return the .
+ } else {
+ // Otherwise, return a with its value set to the current input code point.
+ type = TYPE$F.Delim;
+ offset++;
+ }
+
+ break;
+
+ // U+0027 APOSTROPHE (')
+ case 0x0027:
+ // Consume a string token and return it.
+ consumeStringToken();
+ break;
+
+ // U+0028 LEFT PARENTHESIS (()
+ case 0x0028:
+ // Return a <(-token>.
+ type = TYPE$F.LeftParenthesis;
+ offset++;
+ break;
+
+ // U+0029 RIGHT PARENTHESIS ())
+ case 0x0029:
+ // Return a <)-token>.
+ type = TYPE$F.RightParenthesis;
+ offset++;
+ break;
+
+ // U+002B PLUS SIGN (+)
+ case 0x002B:
+ // If the input stream starts with a number, ...
+ if (isNumberStart(code, getCharCode(offset + 1), getCharCode(offset + 2))) {
+ // ... reconsume the current input code point, consume a numeric token, and return it.
+ consumeNumericToken();
+ } else {
+ // Otherwise, return a with its value set to the current input code point.
+ type = TYPE$F.Delim;
+ offset++;
+ }
+ break;
+
+ // U+002C COMMA (,)
+ case 0x002C:
+ // Return a .
+ type = TYPE$F.Comma;
+ offset++;
+ break;
+
+ // U+002D HYPHEN-MINUS (-)
+ case 0x002D:
+ // If the input stream starts with a number, reconsume the current input code point, consume a numeric token, and return it.
+ if (isNumberStart(code, getCharCode(offset + 1), getCharCode(offset + 2))) {
+ consumeNumericToken();
+ } else {
+ // Otherwise, if the next 2 input code points are U+002D HYPHEN-MINUS U+003E GREATER-THAN SIGN (->), consume them and return a .
+ if (getCharCode(offset + 1) === 0x002D &&
+ getCharCode(offset + 2) === 0x003E) {
+ type = TYPE$F.CDC;
+ offset = offset + 3;
+ } else {
+ // Otherwise, if the input stream starts with an identifier, ...
+ if (isIdentifierStart$1(code, getCharCode(offset + 1), getCharCode(offset + 2))) {
+ // ... reconsume the current input code point, consume an ident-like token, and return it.
+ consumeIdentLikeToken();
+ } else {
+ // Otherwise, return a with its value set to the current input code point.
+ type = TYPE$F.Delim;
+ offset++;
+ }
+ }
+ }
+ break;
+
+ // U+002E FULL STOP (.)
+ case 0x002E:
+ // If the input stream starts with a number, ...
+ if (isNumberStart(code, getCharCode(offset + 1), getCharCode(offset + 2))) {
+ // ... reconsume the current input code point, consume a numeric token, and return it.
+ consumeNumericToken();
+ } else {
+ // Otherwise, return a with its value set to the current input code point.
+ type = TYPE$F.Delim;
+ offset++;
+ }
+
+ break;
+
+ // U+002F SOLIDUS (/)
+ case 0x002F:
+ // If the next two input code point are U+002F SOLIDUS (/) followed by a U+002A ASTERISK (*),
+ if (getCharCode(offset + 1) === 0x002A) {
+ // ... consume them and all following code points up to and including the first U+002A ASTERISK (*)
+ // followed by a U+002F SOLIDUS (/), or up to an EOF code point.
+ type = TYPE$F.Comment;
+ offset = source.indexOf('*/', offset + 2) + 2;
+ if (offset === 1) {
+ offset = source.length;
+ }
+ } else {
+ type = TYPE$F.Delim;
+ offset++;
+ }
+ break;
+
+ // U+003A COLON (:)
+ case 0x003A:
+ // Return a .
+ type = TYPE$F.Colon;
+ offset++;
+ break;
+
+ // U+003B SEMICOLON (;)
+ case 0x003B:
+ // Return a .
+ type = TYPE$F.Semicolon;
+ offset++;
+ break;
+
+ // U+003C LESS-THAN SIGN (<)
+ case 0x003C:
+ // If the next 3 input code points are U+0021 EXCLAMATION MARK U+002D HYPHEN-MINUS U+002D HYPHEN-MINUS (!--), ...
+ if (getCharCode(offset + 1) === 0x0021 &&
+ getCharCode(offset + 2) === 0x002D &&
+ getCharCode(offset + 3) === 0x002D) {
+ // ... consume them and return a .
+ type = TYPE$F.CDO;
+ offset = offset + 4;
+ } else {
+ // Otherwise, return a with its value set to the current input code point.
+ type = TYPE$F.Delim;
+ offset++;
+ }
+
+ break;
+
+ // U+0040 COMMERCIAL AT (@)
+ case 0x0040:
+ // If the next 3 input code points would start an identifier, ...
+ if (isIdentifierStart$1(getCharCode(offset + 1), getCharCode(offset + 2), getCharCode(offset + 3))) {
+ // ... consume a name, create an with its value set to the returned value, and return it.
+ type = TYPE$F.AtKeyword;
+ offset = consumeName(source, offset + 1);
+ } else {
+ // Otherwise, return a with its value set to the current input code point.
+ type = TYPE$F.Delim;
+ offset++;
+ }
+
+ break;
+
+ // U+005B LEFT SQUARE BRACKET ([)
+ case 0x005B:
+ // Return a <[-token>.
+ type = TYPE$F.LeftSquareBracket;
+ offset++;
+ break;
+
+ // U+005C REVERSE SOLIDUS (\)
+ case 0x005C:
+ // If the input stream starts with a valid escape, ...
+ if (isValidEscape(code, getCharCode(offset + 1))) {
+ // ... reconsume the current input code point, consume an ident-like token, and return it.
+ consumeIdentLikeToken();
+ } else {
+ // Otherwise, this is a parse error. Return a with its value set to the current input code point.
+ type = TYPE$F.Delim;
+ offset++;
+ }
+ break;
+
+ // U+005D RIGHT SQUARE BRACKET (])
+ case 0x005D:
+ // Return a <]-token>.
+ type = TYPE$F.RightSquareBracket;
+ offset++;
+ break;
+
+ // U+007B LEFT CURLY BRACKET ({)
+ case 0x007B:
+ // Return a <{-token>.
+ type = TYPE$F.LeftCurlyBracket;
+ offset++;
+ break;
+
+ // U+007D RIGHT CURLY BRACKET (})
+ case 0x007D:
+ // Return a <}-token>.
+ type = TYPE$F.RightCurlyBracket;
+ offset++;
+ break;
+
+ // digit
+ case charCodeCategory.Digit:
+ // Reconsume the current input code point, consume a numeric token, and return it.
+ consumeNumericToken();
+ break;
+
+ // name-start code point
+ case charCodeCategory.NameStart:
+ // Reconsume the current input code point, consume an ident-like token, and return it.
+ consumeIdentLikeToken();
+ break;
+
+ // EOF
+ case charCodeCategory.Eof:
+ // Return an .
+ break;
+
+ // anything else
+ default:
+ // Return a with its value set to the current input code point.
+ type = TYPE$F.Delim;
+ offset++;
+ }
+
+ switch (type) {
+ case balanceCloseType:
+ balancePrev = balanceStart & OFFSET_MASK;
+ balanceStart = balance[balancePrev];
+ balanceCloseType = balanceStart >> TYPE_SHIFT;
+ balance[tokenCount] = balancePrev;
+ balance[balancePrev++] = tokenCount;
+ for (; balancePrev < tokenCount; balancePrev++) {
+ if (balance[balancePrev] === sourceLength) {
+ balance[balancePrev] = tokenCount;
+ }
+ }
+ break;
+
+ case TYPE$F.LeftParenthesis:
+ case TYPE$F.Function:
+ balance[tokenCount] = balanceStart;
+ balanceCloseType = TYPE$F.RightParenthesis;
+ balanceStart = (balanceCloseType << TYPE_SHIFT) | tokenCount;
+ break;
+
+ case TYPE$F.LeftSquareBracket:
+ balance[tokenCount] = balanceStart;
+ balanceCloseType = TYPE$F.RightSquareBracket;
+ balanceStart = (balanceCloseType << TYPE_SHIFT) | tokenCount;
+ break;
+
+ case TYPE$F.LeftCurlyBracket:
+ balance[tokenCount] = balanceStart;
+ balanceCloseType = TYPE$F.RightCurlyBracket;
+ balanceStart = (balanceCloseType << TYPE_SHIFT) | tokenCount;
+ break;
+ }
+
+ offsetAndType[tokenCount++] = (type << TYPE_SHIFT) | offset;
+ }
+
+ // finalize buffers
+ offsetAndType[tokenCount] = (TYPE$F.EOF << TYPE_SHIFT) | offset; //
+ balance[tokenCount] = sourceLength;
+ balance[sourceLength] = sourceLength; // prevents false positive balance match with any token
+ while (balanceStart !== 0) {
+ balancePrev = balanceStart & OFFSET_MASK;
+ balanceStart = balance[balancePrev];
+ balance[balancePrev] = sourceLength;
+ }
+
+ // update stream
+ stream.source = source;
+ stream.firstCharOffset = start;
+ stream.offsetAndType = offsetAndType;
+ stream.tokenCount = tokenCount;
+ stream.balance = balance;
+ stream.reset();
+ stream.next();
+
+ return stream;
+ }
+
+ // extend tokenizer with constants
+ Object.keys(constants$1).forEach(function(key) {
+ tokenize$3[key] = constants$1[key];
+ });
+
+ // extend tokenizer with static methods from utils
+ Object.keys(charCodeDefinitions).forEach(function(key) {
+ tokenize$3[key] = charCodeDefinitions[key];
+ });
+ Object.keys(utils).forEach(function(key) {
+ tokenize$3[key] = utils[key];
+ });
+
+ var tokenizer$3 = tokenize$3;
+
+ var isDigit$3 = tokenizer$3.isDigit;
+ var cmpChar$4 = tokenizer$3.cmpChar;
+ var TYPE$E = tokenizer$3.TYPE;
+
+ var DELIM$6 = TYPE$E.Delim;
+ var WHITESPACE$b = TYPE$E.WhiteSpace;
+ var COMMENT$9 = TYPE$E.Comment;
+ var IDENT$i = TYPE$E.Ident;
+ var NUMBER$9 = TYPE$E.Number;
+ var DIMENSION$7 = TYPE$E.Dimension;
+ var PLUSSIGN$8 = 0x002B; // U+002B PLUS SIGN (+)
+ var HYPHENMINUS$4 = 0x002D; // U+002D HYPHEN-MINUS (-)
+ var N$4 = 0x006E; // U+006E LATIN SMALL LETTER N (n)
+ var DISALLOW_SIGN$1 = true;
+ var ALLOW_SIGN$1 = false;
+
+ function isDelim$1(token, code) {
+ return token !== null && token.type === DELIM$6 && token.value.charCodeAt(0) === code;
+ }
+
+ function skipSC(token, offset, getNextToken) {
+ while (token !== null && (token.type === WHITESPACE$b || token.type === COMMENT$9)) {
+ token = getNextToken(++offset);
+ }
+
+ return offset;
+ }
+
+ function checkInteger$1(token, valueOffset, disallowSign, offset) {
+ if (!token) {
+ return 0;
+ }
+
+ var code = token.value.charCodeAt(valueOffset);
+
+ if (code === PLUSSIGN$8 || code === HYPHENMINUS$4) {
+ if (disallowSign) {
+ // Number sign is not allowed
+ return 0;
+ }
+ valueOffset++;
+ }
+
+ for (; valueOffset < token.value.length; valueOffset++) {
+ if (!isDigit$3(token.value.charCodeAt(valueOffset))) {
+ // Integer is expected
+ return 0;
+ }
+ }
+
+ return offset + 1;
+ }
+
+ // ...
+ // ... ['+' | '-']
+ function consumeB$1(token, offset_, getNextToken) {
+ var sign = false;
+ var offset = skipSC(token, offset_, getNextToken);
+
+ token = getNextToken(offset);
+
+ if (token === null) {
+ return offset_;
+ }
+
+ if (token.type !== NUMBER$9) {
+ if (isDelim$1(token, PLUSSIGN$8) || isDelim$1(token, HYPHENMINUS$4)) {
+ sign = true;
+ offset = skipSC(getNextToken(++offset), offset, getNextToken);
+ token = getNextToken(offset);
+
+ if (token === null && token.type !== NUMBER$9) {
+ return 0;
+ }
+ } else {
+ return offset_;
+ }
+ }
+
+ if (!sign) {
+ var code = token.value.charCodeAt(0);
+ if (code !== PLUSSIGN$8 && code !== HYPHENMINUS$4) {
+ // Number sign is expected
+ return 0;
+ }
+ }
+
+ return checkInteger$1(token, sign ? 0 : 1, sign, offset);
+ }
+
+ // An+B microsyntax https://www.w3.org/TR/css-syntax-3/#anb
+ var genericAnPlusB = function anPlusB(token, getNextToken) {
+ /* eslint-disable brace-style*/
+ var offset = 0;
+
+ if (!token) {
+ return 0;
+ }
+
+ //
+ if (token.type === NUMBER$9) {
+ return checkInteger$1(token, 0, ALLOW_SIGN$1, offset); // b
+ }
+
+ // -n
+ // -n
+ // -n ['+' | '-']
+ // -n-
+ //
+ else if (token.type === IDENT$i && token.value.charCodeAt(0) === HYPHENMINUS$4) {
+ // expect 1st char is N
+ if (!cmpChar$4(token.value, 1, N$4)) {
+ return 0;
+ }
+
+ switch (token.value.length) {
+ // -n
+ // -n
+ // -n ['+' | '-']
+ case 2:
+ return consumeB$1(getNextToken(++offset), offset, getNextToken);
+
+ // -n-
+ case 3:
+ if (token.value.charCodeAt(2) !== HYPHENMINUS$4) {
+ return 0;
+ }
+
+ offset = skipSC(getNextToken(++offset), offset, getNextToken);
+ token = getNextToken(offset);
+
+ return checkInteger$1(token, 0, DISALLOW_SIGN$1, offset);
+
+ //
+ default:
+ if (token.value.charCodeAt(2) !== HYPHENMINUS$4) {
+ return 0;
+ }
+
+ return checkInteger$1(token, 3, DISALLOW_SIGN$1, offset);
+ }
+ }
+
+ // '+'? n
+ // '+'? n
+ // '+'? n ['+' | '-']
+ // '+'? n-
+ // '+'?
+ else if (token.type === IDENT$i || (isDelim$1(token, PLUSSIGN$8) && getNextToken(offset + 1).type === IDENT$i)) {
+ // just ignore a plus
+ if (token.type !== IDENT$i) {
+ token = getNextToken(++offset);
+ }
+
+ if (token === null || !cmpChar$4(token.value, 0, N$4)) {
+ return 0;
+ }
+
+ switch (token.value.length) {
+ // '+'? n
+ // '+'? n
+ // '+'? n ['+' | '-']
+ case 1:
+ return consumeB$1(getNextToken(++offset), offset, getNextToken);
+
+ // '+'? n-
+ case 2:
+ if (token.value.charCodeAt(1) !== HYPHENMINUS$4) {
+ return 0;
+ }
+
+ offset = skipSC(getNextToken(++offset), offset, getNextToken);
+ token = getNextToken(offset);
+
+ return checkInteger$1(token, 0, DISALLOW_SIGN$1, offset);
+
+ // '+'?
+ default:
+ if (token.value.charCodeAt(1) !== HYPHENMINUS$4) {
+ return 0;
+ }
+
+ return checkInteger$1(token, 2, DISALLOW_SIGN$1, offset);
+ }
+ }
+
+ //
+ //
+ //
+ //
+ // ['+' | '-']
+ else if (token.type === DIMENSION$7) {
+ var code = token.value.charCodeAt(0);
+ var sign = code === PLUSSIGN$8 || code === HYPHENMINUS$4 ? 1 : 0;
+
+ for (var i = sign; i < token.value.length; i++) {
+ if (!isDigit$3(token.value.charCodeAt(i))) {
+ break;
+ }
+ }
+
+ if (i === sign) {
+ // Integer is expected
+ return 0;
+ }
+
+ if (!cmpChar$4(token.value, i, N$4)) {
+ return 0;
+ }
+
+ //
+ //
+ // ['+' | '-']
+ if (i + 1 === token.value.length) {
+ return consumeB$1(getNextToken(++offset), offset, getNextToken);
+ } else {
+ if (token.value.charCodeAt(i + 1) !== HYPHENMINUS$4) {
+ return 0;
+ }
+
+ //
+ if (i + 2 === token.value.length) {
+ offset = skipSC(getNextToken(++offset), offset, getNextToken);
+ token = getNextToken(offset);
+
+ return checkInteger$1(token, 0, DISALLOW_SIGN$1, offset);
+ }
+ //
+ else {
+ return checkInteger$1(token, i + 2, DISALLOW_SIGN$1, offset);
+ }
+ }
+ }
+
+ return 0;
+ };
+
+ var isHexDigit$2 = tokenizer$3.isHexDigit;
+ var cmpChar$3 = tokenizer$3.cmpChar;
+ var TYPE$D = tokenizer$3.TYPE;
+
+ var IDENT$h = TYPE$D.Ident;
+ var DELIM$5 = TYPE$D.Delim;
+ var NUMBER$8 = TYPE$D.Number;
+ var DIMENSION$6 = TYPE$D.Dimension;
+ var PLUSSIGN$7 = 0x002B; // U+002B PLUS SIGN (+)
+ var HYPHENMINUS$3 = 0x002D; // U+002D HYPHEN-MINUS (-)
+ var QUESTIONMARK$2 = 0x003F; // U+003F QUESTION MARK (?)
+ var U$2 = 0x0075; // U+0075 LATIN SMALL LETTER U (u)
+
+ function isDelim(token, code) {
+ return token !== null && token.type === DELIM$5 && token.value.charCodeAt(0) === code;
+ }
+
+ function startsWith$1(token, code) {
+ return token.value.charCodeAt(0) === code;
+ }
+
+ function hexSequence(token, offset, allowDash) {
+ for (var pos = offset, hexlen = 0; pos < token.value.length; pos++) {
+ var code = token.value.charCodeAt(pos);
+
+ if (code === HYPHENMINUS$3 && allowDash && hexlen !== 0) {
+ if (hexSequence(token, offset + hexlen + 1, false) > 0) {
+ return 6; // dissallow following question marks
+ }
+
+ return 0; // dash at the ending of a hex sequence is not allowed
+ }
+
+ if (!isHexDigit$2(code)) {
+ return 0; // not a hex digit
+ }
+
+ if (++hexlen > 6) {
+ return 0; // too many hex digits
+ } }
+
+ return hexlen;
+ }
+
+ function withQuestionMarkSequence(consumed, length, getNextToken) {
+ if (!consumed) {
+ return 0; // nothing consumed
+ }
+
+ while (isDelim(getNextToken(length), QUESTIONMARK$2)) {
+ if (++consumed > 6) {
+ return 0; // too many question marks
+ }
+
+ length++;
+ }
+
+ return length;
+ }
+
+ // https://drafts.csswg.org/css-syntax/#urange
+ // Informally, the production has three forms:
+ // U+0001
+ // Defines a range consisting of a single code point, in this case the code point "1".
+ // U+0001-00ff
+ // Defines a range of codepoints between the first and the second value, in this case
+ // the range between "1" and "ff" (255 in decimal) inclusive.
+ // U+00??
+ // Defines a range of codepoints where the "?" characters range over all hex digits,
+ // in this case defining the same as the value U+0000-00ff.
+ // In each form, a maximum of 6 digits is allowed for each hexadecimal number (if you treat "?" as a hexadecimal digit).
+ //
+ // =
+ // u '+' '?'* |
+ // u '?'* |
+ // u '?'* |
+ // u |
+ // u |
+ // u '+' '?'+
+ var genericUrange = function urange(token, getNextToken) {
+ var length = 0;
+
+ // should start with `u` or `U`
+ if (token === null || token.type !== IDENT$h || !cmpChar$3(token.value, 0, U$2)) {
+ return 0;
+ }
+
+ token = getNextToken(++length);
+ if (token === null) {
+ return 0;
+ }
+
+ // u '+' '?'*
+ // u '+' '?'+
+ if (isDelim(token, PLUSSIGN$7)) {
+ token = getNextToken(++length);
+ if (token === null) {
+ return 0;
+ }
+
+ if (token.type === IDENT$h) {
+ // u '+' '?'*
+ return withQuestionMarkSequence(hexSequence(token, 0, true), ++length, getNextToken);
+ }
+
+ if (isDelim(token, QUESTIONMARK$2)) {
+ // u '+' '?'+
+ return withQuestionMarkSequence(1, ++length, getNextToken);
+ }
+
+ // Hex digit or question mark is expected
+ return 0;
+ }
+
+ // u '?'*
+ // u
+ // u
+ if (token.type === NUMBER$8) {
+ if (!startsWith$1(token, PLUSSIGN$7)) {
+ return 0;
+ }
+
+ var consumedHexLength = hexSequence(token, 1, true);
+ if (consumedHexLength === 0) {
+ return 0;
+ }
+
+ token = getNextToken(++length);
+ if (token === null) {
+ // u
+ return length;
+ }
+
+ if (token.type === DIMENSION$6 || token.type === NUMBER$8) {
+ // u
+ // u
+ if (!startsWith$1(token, HYPHENMINUS$3) || !hexSequence(token, 1, false)) {
+ return 0;
+ }
+
+ return length + 1;
+ }
+
+ // u '?'*
+ return withQuestionMarkSequence(consumedHexLength, length, getNextToken);
+ }
+
+ // u '?'*
+ if (token.type === DIMENSION$6) {
+ if (!startsWith$1(token, PLUSSIGN$7)) {
+ return 0;
+ }
+
+ return withQuestionMarkSequence(hexSequence(token, 1, true), ++length, getNextToken);
+ }
+
+ return 0;
+ };
+
+ var tokenizer$2 = tokenizer$3;
+ var isIdentifierStart = tokenizer$2.isIdentifierStart;
+ var isHexDigit$1 = tokenizer$2.isHexDigit;
+ var isDigit$2 = tokenizer$2.isDigit;
+ var cmpStr$3 = tokenizer$2.cmpStr;
+ var consumeNumber$3 = tokenizer$2.consumeNumber;
+ var TYPE$C = tokenizer$2.TYPE;
+ var anPlusB = genericAnPlusB;
+ var urange = genericUrange;
+
+ var cssWideKeywords$1 = ['unset', 'initial', 'inherit'];
+ var calcFunctionNames = ['calc(', '-moz-calc(', '-webkit-calc('];
+
+ // https://www.w3.org/TR/css-values-3/#lengths
+ var LENGTH = {
+ // absolute length units
+ 'px': true,
+ 'mm': true,
+ 'cm': true,
+ 'in': true,
+ 'pt': true,
+ 'pc': true,
+ 'q': true,
+
+ // relative length units
+ 'em': true,
+ 'ex': true,
+ 'ch': true,
+ 'rem': true,
+
+ // viewport-percentage lengths
+ 'vh': true,
+ 'vw': true,
+ 'vmin': true,
+ 'vmax': true,
+ 'vm': true
+ };
+
+ var ANGLE = {
+ 'deg': true,
+ 'grad': true,
+ 'rad': true,
+ 'turn': true
+ };
+
+ var TIME = {
+ 's': true,
+ 'ms': true
+ };
+
+ var FREQUENCY = {
+ 'hz': true,
+ 'khz': true
+ };
+
+ // https://www.w3.org/TR/css-values-3/#resolution (https://drafts.csswg.org/css-values/#resolution)
+ var RESOLUTION = {
+ 'dpi': true,
+ 'dpcm': true,
+ 'dppx': true,
+ 'x': true // https://github.com/w3c/csswg-drafts/issues/461
+ };
+
+ // https://drafts.csswg.org/css-grid/#fr-unit
+ var FLEX = {
+ 'fr': true
+ };
+
+ // https://www.w3.org/TR/css3-speech/#mixing-props-voice-volume
+ var DECIBEL = {
+ 'db': true
+ };
+
+ // https://www.w3.org/TR/css3-speech/#voice-props-voice-pitch
+ var SEMITONES = {
+ 'st': true
+ };
+
+ // safe char code getter
+ function charCode(str, index) {
+ return index < str.length ? str.charCodeAt(index) : 0;
+ }
+
+ function eqStr(actual, expected) {
+ return cmpStr$3(actual, 0, actual.length, expected);
+ }
+
+ function eqStrAny(actual, expected) {
+ for (var i = 0; i < expected.length; i++) {
+ if (eqStr(actual, expected[i])) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ // IE postfix hack, i.e. 123\0 or 123px\9
+ function isPostfixIeHack(str, offset) {
+ if (offset !== str.length - 2) {
+ return false;
+ }
+
+ return (
+ str.charCodeAt(offset) === 0x005C && // U+005C REVERSE SOLIDUS (\)
+ isDigit$2(str.charCodeAt(offset + 1))
+ );
+ }
+
+ function outOfRange(opts, value, numEnd) {
+ if (opts && opts.type === 'Range') {
+ var num = Number(
+ numEnd !== undefined && numEnd !== value.length
+ ? value.substr(0, numEnd)
+ : value
+ );
+
+ if (isNaN(num)) {
+ return true;
+ }
+
+ if (opts.min !== null && num < opts.min) {
+ return true;
+ }
+
+ if (opts.max !== null && num > opts.max) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ function consumeFunction(token, getNextToken) {
+ var startIdx = token.index;
+ var length = 0;
+
+ // balanced token consuming
+ do {
+ length++;
+
+ if (token.balance <= startIdx) {
+ break;
+ }
+ } while (token = getNextToken(length));
+
+ return length;
+ }
+
+ // TODO: implement
+ // can be used wherever , , ,