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…

+ + +
+
+ + +
+ LASER + 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

+ + + + + + + + + + + + + + +
AnoLançamentoO que mudou
2014ESP8266Wi-Fi barato ao alcance de qualquer projeto. Ainda muito usado.
2016ESP32Dois núcleos, Bluetooth e periféricos de verdade. O marco da plataforma.
2019ESP32-S2Foco em segurança e USB nativo; abre mão do Bluetooth.
2020ESP32-C3Primeira série RISC-V. Marca a virada de arquitetura da empresa.
2021ESP32-S3Dois núcleos com instruções vetoriais para IA. Vira o carro-chefe.
2022ESP32-C6, H2Chega o rádio 802.15.4: Zigbee, Thread e Matter entram na família.
2023ESP32-P4Alto desempenho sem rádio, com MIPI para câmera e display.
2024 e 2025C5, C61, H4, S31Wi-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: +

+ + + + + + + + + + + + + + + + + + + + + + + + + +
TermoO 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óduloO 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 desenvolvimentoO 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érieA 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. +

+
+ +
+ Diagrama do ESP32-S3-DevKitC-1 com a pinagem completa, mostrando o módulo ESP32-S3-WROOM soldado no centro da placa. +
+ 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 oficial do ESP32-S3: CPU principal, memórias, rádio Wi-Fi e Bluetooth, cadeia de RF, periféricos, domínio RTC e bloco de segurança. +
+ 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óriaPara que serveOrdem de grandeza
SRAMMemó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
ROMGravada 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 externaOnde 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
PSRAMRAM 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 chip + + + Processador + SRAM e ROM internas + + + Cache + MMU + traz o código para dentro + + + Matriz de roteamento: função quase livre por pino + + + + + + + + + + 6 + + SPI + CS, CLK, D, Q, WP e HD + + + Flash externa + o seu firmware mora aqui + + + PSRAM (opcional) + buffers grandes + +
+ 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: +

+ + + + + + + + + + +
TecnologiaPara 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 LEComunicaçã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.4Base 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. +

+ +
+ Diagrama de pinagem do ESP32-DevKitC, com legenda indicando pinos de flash, strapping, somente entrada e depuração. +
+ 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: +

+ + + + + + + + + + + +
GrupoPinos
Flash e PSRAMGPIO26, 27, 28, 29, 30, 31 e 32
StrappingGPIO0, GPIO45 e GPIO46
Gravação e JTAGGPIO39, 40, 41, 42 (JTAG) e GPIO43, 44 (UART0)
USB nativoGPIO19 e GPIO20
Somente entradaNenhum 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.

+
+
+ + + +
+ + + 1. Energização + CHIP_EN em nível alto + + + 2. Bootloader da ROM + lê os pinos de strapping + + + 3. Segundo estágio + partições, assinatura, + descriptografia + + + 4. Sua aplicação + app_main() ou + setup() + loop() + + + + + + + + + + Modo de gravação + espera firmware pela serial + strapping + em nível de boot + + É este desvio que os botões BOOT e RESET forçam na mã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érieNúcleoClockSRAMGPIOESP-IDF mínimo
ESP32Xtensa LX6 ×2240 MHz520 KB34v3.x
ESP32-S2Xtensa LX7 ×1240 MHz320 KB43v4.2
ESP32-S3Xtensa LX7 ×2240 MHz512 KB45v4.4
ESP32-S31RISC-V ×2320 MHz512 KB60master
ESP32-C2RISC-V ×1120 MHz272 KB14v5.0
ESP32-C3RISC-V ×1160 MHz400 KB22v4.3
ESP32-C5RISC-V ×1240 MHz384 KB29v5.5.2
ESP32-C6RISC-V ×1160 MHz512 KB30v5.1
ESP32-C61RISC-V ×1160 MHz320 KB30v5.5.2
ESP32-P4RISC-V ×2400 MHz768 KB55v5.3
ESP32-H2RISC-V ×196 MHz320 KB19v5.1
ESP32-H4RISC-V ×296 MHz384 KB40master
"master" indica série que ainda não entrou em versão estável do ESP-IDF.
+ +

Rádios

+ + + + + + + + + + + + + + + + +
SérieWi-FiBluetooth802.15.4Matter
ESP32Wi-Fi 4BLE + ClássicoNãoNão
ESP32-S2Wi-Fi 4NãoNãoNão
ESP32-S3Wi-Fi 4BLE 5.4NãoNão
ESP32-S31Wi-Fi 6BLE + ClássicoSimSim
ESP32-C2Wi-Fi 4BLE 5.3NãoNão
ESP32-C3Wi-Fi 4BLE 5.4NãoNão
ESP32-C5Wi-Fi 6 · 2,4 e 5 GHzBLE 6.0SimSim
ESP32-C6Wi-Fi 6BLE 6.0SimSim
ESP32-C61Wi-Fi 6BLE 6.0NãoVia Wi-Fi
ESP32-P4NãoNãoNãoNão
ESP32-H2NãoBLE 6.0SimSim
ESP32-H4NãoBLE 5.4SimSim
+ +

Ferramentas e encapsulamento

+ + + + + + + + + + + + + + + + + +
SérieCore ArduinoDepuraçãoEncapsulamento
ESP32147 placasSonda externaQFN48 (6×6 e 5×5 mm)
ESP32-S226 placasSonda externaQFN56 (7×7 mm)
ESP32-S3158 placasUSB integradoQFN56 (7×7 mm)
ESP32-S31NãoUSB integradoQFN80 (8×8 mm)
ESP32-C21 placaSonda externaQFN24 (4×4 mm)
ESP32-C329 placasUSB integradoQFN32 (5×5) e QFN28 (4×4 mm)
ESP32-C510 placasUSB integradoQFN48 (6×6 mm)
ESP32-C621 placasUSB integradoQFN40 e QFN32 (5×5 mm)
ESP32-C611 placaUSB integradoQFN40 (5×5 mm)
ESP32-P47 placasUSB integradoQFN104 (10×10 mm)
ESP32-H23 placasUSB integradoQFN32 (4×4 mm)
ESP32-H4NãoUSB integradoNã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

+ + + + + + + + + + + + + + + + + +
BlocoPara que serve
AESCifra simétrica: é o que protege o tráfego depois que uma conexão TLS é estabelecida.
SHAFunções de resumo (hash). Base de assinaturas, verificação de integridade e HMAC.
RSA / MPIAritmética de números grandes, usada em chave pública clássica. É o que acelera o handshake TLS.
ECCCriptografia de curvas elípticas: mesma segurança do RSA com chaves muito menores, o que importa bastante em microcontrolador.
ECDSAAssinatura digital sobre curvas elípticas. É o que o Matter exige para o atestado de dispositivo.
HMACAutenticação de mensagem com chave secreta.
Digital SignatureAssina usando uma chave RSA guardada em eFuse, sem que o firmware chegue a vê-la.
Key ManagerProvisiona e usa chaves sem que elas fiquem legíveis para o firmware, nem se alguém extrair a imagem.
RNGGerador de números verdadeiramente aleatórios. Sem ele, toda a criptografia fica previsível.
Criptografia de flashCifra o conteúdo da flash, impedindo que alguém leia o firmware soldando um gravador na memória.
Secure BootO chip se recusa a executar firmware que não esteja assinado pela sua chave.
Proteção DPADefesa contra ataque que deduz a chave medindo variações no consumo de energia durante a operação.
+ +

O que cada série tem

+ + + + + + + + + + + + + + + + + + +
SérieAESECCECDSAKey ManagerSecure Boot
ESP32128/192/256NãoNãoNãoV1 (legado)
ESP32-S2128/192/256 + GCMNãoNãoNãoV2 · RSA-3072
ESP32-S3128/256NãoNãoNãoV2 · RSA-3072
ESP32-S31128/256 + GCMP-192/256/384SimSimV2 · RSA-3072
ESP32-C2NãoP-192/256NãoNãoV2 · ECDSA
ESP32-C3128/256NãoNãoNãoV2 · RSA-3072
ESP32-C5128/256P-192/256/384SimSimV2 · RSA/ECDSA
ESP32-C6128/256P-192/256NãoNãoV2 · RSA/ECDSA
ESP32-C61NãoP-192/256SimNãoV2 · ECDSA
ESP32-P4128/256 + GCMP-192/256/384SimSimV2 · RSA/ECDSA
ESP32-H2128/256P-192/256SimNãoV2 · RSA/ECDSA
ESP32-H4128/256P-192/256/384SimNãoPendente
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 R3ESP8266ESP32-S3STM32F103
NúcleoAVR 8 bitsXtensa L106Xtensa LX7 ×2Cortex-M3
Clock16 MHz80 a 160 MHz240 MHz72 MHz
SRAM2 KB~50 KB úteis512 KB20 KB
Flash32 KBExterna, 1 a 4 MBExterna, 4 a 16 MB64 a 128 KB
Wi-FiNãoSimSimNão
BluetoothNãoNãoBLE 5Não
Tensão de E/S5 V3,3 V3,3 V3,3 V (alguns 5 V)
ADC10 bits, linear1 canal, 10 bits12 bits, calibrar12 bits, bom
GPIO úteis20~94537
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. +

+ + + + + + + + + + + +
NecessidadeSéries que atendem
Wi-Fi + Bluetooth LEESP32, S3, C3, C6, C5, C61, S31
Bluetooth Clássico (áudio, SPP)ESP32, S31
Zigbee / Thread / Matter em malhaC6, C5, H2, H4, S31
Wi-Fi 5 GHzC5
Ethernet cabeadaESP32, P4
Sem rádio, só processamentoP4
+ +

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 porPor quê
Primeiro projeto, aprendendoESP32-S3Muita RAM, USB nativo para depurar e o maior ecossistema de exemplos
Sensor Wi-Fi simples e baratoESP32-C3Faz o essencial pelo menor preço, com boa maturidade
Automação residencial / MatterESP32-C6Único caminho maduro com Wi-Fi 6 e 802.15.4 juntos
Sensor a pilha, sem Wi-FiESP32-H2Sem o rádio mais custoso, a autonomia muda de patamar
Tela, câmera ou interface gráficaESP32-P4 ou S3MIPI e RAM suficiente para buffer de vídeo
Áudio por Bluetooth ClássicoESP32Praticamente a única série com BR/EDR
Visão computacional na bordaESP32-S3Instruções vetoriais e PSRAM para o buffer da câmera
Produto em altíssimo volumeESP32-C2O 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 ArduinoESP-IDF
Curva de entradaBaixa, familiarÍngreme: menuconfig, CMake, componentes
Controle do hardwareParcialTotal
Acesso a recursos novosChega depoisImediato
Bibliotecas de sensoresEnormeMenor, mais técnica
Consumo de memóriaMaiorAjustável
Indicado paraAprender, prototiparProduto, 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

+ + + + + + + + + + + +
ItemQtd.Observação
ESP32-S3-DevKitC-11Qualquer variante de flash/PSRAM serve
Sensor DHT111O componente azul de 4 pinos
Resistor de 10 kΩ1Pull-up do dado (marrom, preto, laranja)
Protoboard1Meia protoboard basta
Jumpers4Vermelho, preto e amarelo, por convenção
Cabo USB1De dados. Cabo só de carga não funciona
+ +

Montagem

+ + + + + + + + + + +
LigaçãoCor na figuraFunção
VCC do DHT11 → 3V3VermelhoAlimentação do sensor
DATA do DHT11 → GPIO12AmareloLinha de dados bidirecional
GND do DHT11 → GNDPretoReferência de terra
10 kΩ entre DATA e 3V3ResistorMantém a linha em nível alto em repouso
O terceiro pino do DHT11 (contando da esquerda) não é usado.
+ +
+ Esquema de ligação do DHT11 ao ESP32-S3-DevKitC: fio vermelho do VCC ao 3V3, fio amarelo do DATA ao GPIO12, fio preto do GND ao GND, e um resistor de 10 kΩ entre a linha de dados e o 3V3. +
+ 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. +

+ Arquivo editável do Fritzing: + baixar pratica.fzz +
+
+ +
+
Por que o resistor de pull-up é necessário
+

+ 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: +

+ +
+ + + + + + + alto + baixo + + + + + + + + + + 18 ms + 80 µs + 70 µs + 26 µs + + o ESP32 pede a leitura + o sensor responde + bit 1 + bit 0 + …40 bits + + + a partir daqui, o valor do bit está na largura do pulso alto + +
+ 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. +
+
+
+ +
    +
  1. O ESP32 puxa a linha para baixo por cerca de 18 ms, que é o "acorda" do sensor.
  2. +
  3. Solta a linha; o pull-up a leva para alto.
  4. +
  5. O DHT11 responde puxando para baixo por 80 µs e soltando por mais 80 µs.
  6. +
  7. Em seguida envia 40 bits: umidade (2 bytes), temperatura (2 bytes) e + um byte de verificação.
  8. +
  9. Cada bit é codificado pela duração do nível alto: cerca de 26 µs é zero, + cerca de 70 µs é um.
  10. +
+ +

+ 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": +

+ +
https://espressif.github.io/arduino-esp32/package_esp32_index.json
+

+ 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);
+
+void setup() {
+  Serial.begin(115200);
+  delay(1000);              // tempo para a USB enumerar
+  dht.begin();
+  Serial.println("Leitor DHT11 - ESP32-S3");
+}
+
+void loop() {
+  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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SintomaCausa provável e solução
Monitor serial vazioCabo na porta USB nativa sem USB CDC On Boot habilitado. Veja o passo + 4. Confira também a velocidade em 115200.
"Falha na leitura" sempreFio de dados no pino errado, VCC não conectado, ou resistor de pull-up ausente + ou mal encaixado na protoboard.
Leitura falha às vezesJumper com mau contato, fio de dados muito longo, ou delay menor + que 2 segundos.
A porta não apareceCabo só de carga (troque por um de dados) ou driver do conversor USB-serial + faltando.
Erro ao gravar, timeoutPlaca não entrou em modo de gravação: segure BOOT, pressione e solte RESET, + solte BOOT e tente de novo.
Temperatura sempre inteiraNão é defeito. O DHT11 tem resolução de 1 °C. O DHT22 dá 0,1 °C.
Valores absurdos e constantesSensor 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:

+ +
    +
  1. Acenda o LED RGB da placa em vermelho quando a temperatura passar de 28 °C e em azul + abaixo disso.
  2. +
  3. Calcule e imprima a média das últimas dez leituras, para suavizar o ruído.
  4. +
  5. Troque o delay por controle de tempo com millis(), de modo + que o programa continue respondendo enquanto espera.
  6. +
  7. Detecte e conte quantas vezes a umidade subiu mais de 5 % entre leituras consecutivas.
  8. +
  9. Publique a leitura numa página web servida pelo próprio ESP32, acessível por qualquer + aparelho na mesma rede.
  10. +
+
+ + +
+
+
10
+
+

Glossário e próximos passos

+

Os termos que aparecem o tempo todo, e para onde seguir.

+
+
+ +

Glossário

+ + + + + + + + + + + + + + + + + + + + + +
TermoSignificado
SoCSystem on Chip: um único chip reunindo processador, memória, rádio e periféricos.
GPIOPino de entrada e saída de uso geral.
StrappingPino lido no reset para definir o modo de boot.
Pull-up / pull-downResistor que define o nível de uma linha em repouso.
ULP / LP coreCoprocessador de baixíssimo consumo que roda com o chip principal dormindo.
PSRAMMemória RAM externa opcional, para buffers grandes.
eFuseMemória que só pode ser gravada uma vez, usada para chaves e configurações permanentes.
Deep sleepModo em que quase tudo desliga, mantendo só o mínimo para acordar depois.
OTAOver-The-Air: atualização de firmware pela rede.
FreeRTOSSistema operacional de tempo real que gerencia as tarefas do ESP32.
WatchdogTemporizador que reinicia o chip se uma tarefa travar.
TWAINome que a Espressif dá ao controlador compatível com CAN.
MatterPadrão de interoperabilidade para casa conectada, sobre Wi-Fi ou Thread.
Thread / ZigbeeProtocolos de rede em malha de baixo consumo sobre IEEE 802.15.4.
Secure BootMecanismo que impede o chip de executar firmware não assinado.
ErrataDocumento com os defeitos conhecidos de uma revisão do silício.
+ +

Onde consultar

+ + + + + + + + + + + + +
FontePara quê
ESPDocsEspecificações, pinagem, segurança e comparação das séries em português. Em espdocs.cienciaembarcada.com.br
Datasheet da sérieVerdade final sobre limites elétricos, pinagem e encapsulamento
Technical Reference ManualDetalhe de registradores e comportamento de cada periférico
ESP-IDF Programming GuideAPI oficial, exemplos e guias de Wi-Fi, BLE, OTA e segurança
Errata do chipDefeitos conhecidos daquela revisão. Leia antes de fechar projeto
Fórum esp32.comDúvidas específicas, com participação de engenheiros da Espressif
GitHub da EspressifCó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: +

+ +
    +
  1. 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.
  2. +
  3. 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.
  4. +
  5. Economizar energia com deep sleep, medindo de verdade quanto tempo uma + bateria aguenta.
  6. +
  7. Atualizar o firmware pelo ar com OTA, fechando o ciclo de um produto + que não precisa mais de cabo depois de instalado.
  8. +
  9. Migrar um projeto para o ESP-IDF, para enxergar o que o core Arduino + estava escondendo.
  10. +
+ +
+
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 , , ,