Gerador de Lorem Ipsum em Zig — Tutorial Passo a Passo

Gerador de Lorem Ipsum em Zig — Tutorial Passo a Passo

Neste tutorial, vamos construir um gerador de texto Lorem Ipsum no terminal. O programa gera texto placeholder com quantidade configurável de parágrafos, frases e palavras. Este projeto é ideal para praticar manipulação de arrays, geração aleatória e formatação de saída em Zig.

O Que Vamos Construir

Nosso gerador vai:

  • Gerar parágrafos de texto Lorem Ipsum realista
  • Permitir configurar número de parágrafos, frases por parágrafo e palavras por frase
  • Incluir o parágrafo clássico “Lorem ipsum dolor sit amet…” como primeira opção
  • Usar um vocabulário latino para gerar texto que parece natural
  • Suportar saída formatada com quebras de linha adequadas

Por Que Este Projeto?

Este projeto nos ensina a trabalhar com arrays de strings constantes em comptime, geração de números aleatórios para seleção de elementos, e construção de strings dinâmicas com ArrayList. É um projeto leve mas que cobre padrões muito úteis em Zig.

Pré-requisitos

Passo 1: Estrutura do Projeto

mkdir gerador-lorem-ipsum
cd gerador-lorem-ipsum
zig init

Passo 2: Vocabulário Latino

O Lorem Ipsum usa um vocabulário pseudo-latino. Definimos as palavras como um array constante em comptime.

const std = @import("std");
const io = std.io;
const fmt = std.fmt;
const mem = std.mem;
const ArrayList = std.ArrayList;

/// Vocabulário de palavras latinas usadas no Lorem Ipsum.
/// Em Zig, arrays constantes de strings são armazenados em .rodata
/// e não ocupam memória heap. O compilador conhece o tamanho total
/// em tempo de compilação.
const vocabulario = [_][]const u8{
    "lorem",      "ipsum",     "dolor",      "sit",       "amet",
    "consectetur", "adipiscing", "elit",      "sed",       "do",
    "eiusmod",    "tempor",    "incididunt", "ut",        "labore",
    "et",         "dolore",    "magna",      "aliqua",    "enim",
    "ad",         "minim",     "veniam",     "quis",      "nostrud",
    "exercitation", "ullamco",  "laboris",   "nisi",      "aliquip",
    "ex",         "ea",        "commodo",    "consequat", "duis",
    "aute",       "irure",     "in",         "reprehenderit", "voluptate",
    "velit",      "esse",      "cillum",     "fugiat",    "nulla",
    "pariatur",   "excepteur", "sint",       "occaecat",  "cupidatat",
    "non",        "proident",  "sunt",       "culpa",     "qui",
    "officia",    "deserunt",  "mollit",     "anim",      "id",
    "est",        "laborum",   "ac",         "ante",      "bibendum",
    "blandit",    "congue",    "cras",       "cursus",    "dapibus",
    "diam",       "dictum",    "dignissim",  "donec",     "egestas",
    "elementum",  "etiam",     "euismod",    "facilisis", "faucibus",
    "felis",      "fermentum", "fringilla",  "gravida",   "habitant",
    "hendrerit",  "iaculis",   "imperdiet",  "integer",   "interdum",
    "justo",      "lacinia",   "lacus",      "laoreet",   "lectus",
    "leo",        "libero",    "ligula",     "lobortis",  "luctus",
    "maecenas",   "massa",     "mattis",     "mauris",    "metus",
};

/// O parágrafo clássico de Lorem Ipsum que sempre inicia o texto.
const paragrafo_classico =
    "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do " ++
    "eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim " ++
    "ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " ++
    "aliquip ex ea commodo consequat. Duis aute irure dolor in " ++
    "reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla " ++
    "pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " ++
    "culpa qui officia deserunt mollit anim id est laborum.";

Passo 3: Configuração do Gerador

/// Configuração para geração de texto.
/// Valores padrão produzem texto com aparência realista.
const ConfigGeracao = struct {
    paragrafos: u32 = 3,
    frases_por_paragrafo_min: u32 = 3,
    frases_por_paragrafo_max: u32 = 7,
    palavras_por_frase_min: u32 = 5,
    palavras_por_frase_max: u32 = 15,
    iniciar_com_classico: bool = true,

    /// Valida a configuração.
    pub fn validar(self: ConfigGeracao) !void {
        if (self.paragrafos == 0 or self.paragrafos > 100) return error.ParagrafosInvalidos;
        if (self.frases_por_paragrafo_min == 0) return error.FrasesInvalidas;
        if (self.frases_por_paragrafo_min > self.frases_por_paragrafo_max) return error.FrasesInvalidas;
        if (self.palavras_por_frase_min < 3) return error.PalavrasInvalidas;
        if (self.palavras_por_frase_min > self.palavras_por_frase_max) return error.PalavrasInvalidas;
    }
};

Passo 4: Gerador de Texto

/// Gerador de texto Lorem Ipsum.
/// Usa um PRNG para selecionar palavras aleatórias do vocabulário
/// e as organiza em frases e parágrafos com formatação adequada.
const GeradorLorem = struct {
    rng: std.Random,
    config: ConfigGeracao,

    const Self = @This();

    pub fn init(rng: std.Random, config: ConfigGeracao) Self {
        return .{ .rng = rng, .config = config };
    }

    /// Seleciona uma palavra aleatória do vocabulário.
    fn palavraAleatoria(self: *Self) []const u8 {
        const idx = self.rng.intRangeAtMost(usize, 0, vocabulario.len - 1);
        return vocabulario[idx];
    }

    /// Gera um número aleatório dentro de um intervalo.
    fn intAleatorio(self: *Self, min: u32, max: u32) u32 {
        return self.rng.intRangeAtMost(u32, min, max);
    }

    /// Capitaliza a primeira letra de um slice, escrevendo no buffer.
    fn capitalizar(palavra: []const u8, buf: []u8) []const u8 {
        if (palavra.len == 0) return "";
        const len = @min(palavra.len, buf.len);
        @memcpy(buf[0..len], palavra[0..len]);
        if (buf[0] >= 'a' and buf[0] <= 'z') {
            buf[0] -= 32; // ASCII: 'a' - 'A' = 32
        }
        return buf[0..len];
    }

    /// Gera uma frase aleatória.
    /// A frase começa com maiúscula e termina com ponto.
    pub fn gerarFrase(self: *Self, writer: anytype) !void {
        const num_palavras = self.intAleatorio(
            self.config.palavras_por_frase_min,
            self.config.palavras_por_frase_max,
        );

        var i: u32 = 0;
        while (i < num_palavras) : (i += 1) {
            const palavra = self.palavraAleatoria();

            if (i == 0) {
                // Capitalizar primeira palavra
                var buf: [64]u8 = undefined;
                const cap = capitalizar(palavra, &buf);
                try writer.print("{s}", .{cap});
            } else {
                try writer.print(" {s}", .{palavra});
            }

            // Inserir vírgula ocasionalmente (não na última palavra)
            if (i > 0 and i < num_palavras - 1 and self.rng.intRangeAtMost(u32, 0, 7) == 0) {
                try writer.print(",", .{});
            }
        }

        try writer.print(". ", .{});
    }

    /// Gera um parágrafo completo.
    pub fn gerarParagrafo(self: *Self, writer: anytype) !void {
        const num_frases = self.intAleatorio(
            self.config.frases_por_paragrafo_min,
            self.config.frases_por_paragrafo_max,
        );

        var i: u32 = 0;
        while (i < num_frases) : (i += 1) {
            try self.gerarFrase(writer);
        }
    }

    /// Gera o texto completo conforme a configuração.
    pub fn gerar(self: *Self, writer: anytype) !void {
        var p: u32 = 0;
        while (p < self.config.paragrafos) : (p += 1) {
            if (p == 0 and self.config.iniciar_com_classico) {
                try writer.print("{s}", .{paragrafo_classico});
            } else {
                try self.gerarParagrafo(writer);
            }
            try writer.print("\n", .{});
            if (p < self.config.paragrafos - 1) {
                try writer.print("\n", .{});
            }
        }
    }
};

Decisão de design: O gerador escreve diretamente para um writer em vez de retornar uma string alocada. Isso é idiomático em Zig — usamos writer interfaces para evitar alocações desnecessárias e permitir que o chamador decida onde o texto vai (stdout, arquivo, buffer).

Passo 5: Contadores e Estatísticas

/// Conta palavras, frases e caracteres em um texto.
const EstatisticasTexto = struct {
    caracteres: usize = 0,
    palavras: usize = 0,
    frases: usize = 0,
    paragrafos: usize = 0,

    pub fn analisar(texto: []const u8) EstatisticasTexto {
        var stats = EstatisticasTexto{};
        var em_palavra = false;

        stats.caracteres = texto.len;
        if (texto.len > 0) stats.paragrafos = 1;

        for (texto, 0..) |c, i| {
            // Contar palavras
            if (c != ' ' and c != '\n' and c != '\t') {
                if (!em_palavra) {
                    stats.palavras += 1;
                    em_palavra = true;
                }
            } else {
                em_palavra = false;
            }

            // Contar frases (terminam com . ! ?)
            if (c == '.' or c == '!' or c == '?') {
                stats.frases += 1;
            }

            // Contar parágrafos (duas quebras de linha)
            if (c == '\n' and i + 1 < texto.len and texto[i + 1] == '\n') {
                stats.paragrafos += 1;
            }
        }

        return stats;
    }
};

Passo 6: Interface CLI

pub fn main() !void {
    const stdout = io.getStdOut().writer();
    const stdin = io.getStdIn().reader();

    var prng = std.Random.DefaultPrng.init(blk: {
        var seed: u64 = undefined;
        std.posix.getrandom(std.mem.asBytes(&seed)) catch unreachable;
        break :blk seed;
    });

    try stdout.print(
        \\
        \\  ==========================================
        \\     GERADOR DE LOREM IPSUM - Zig
        \\  ==========================================
        \\
    , .{});

    var buf: [64]u8 = undefined;

    while (true) {
        try stdout.print(
            \\
            \\  [1] Gerar com padrao (3 paragrafos)
            \\  [2] Gerar personalizado
            \\  [3] Gerar apenas palavras
            \\  [4] Gerar apenas frases
            \\  [5] Sair
            \\
            \\  Opcao:
        , .{});

        const opcao_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse break;
        const opcao = mem.trim(u8, opcao_raw, " \t\r\n");

        if (mem.eql(u8, opcao, "5")) break;

        var config = ConfigGeracao{};

        if (mem.eql(u8, opcao, "2")) {
            try stdout.print("\n  Numero de paragrafos (1-20): ", .{});
            const p_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse continue;
            if (fmt.parseInt(u32, mem.trim(u8, p_raw, " \t\r\n"), 10)) |v| {
                config.paragrafos = v;
            } else |_| {}

            try stdout.print("  Iniciar com paragrafo classico? (s/n): ", .{});
            const c_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse continue;
            const c_str = mem.trim(u8, c_raw, " \t\r\n");
            config.iniciar_com_classico = !mem.eql(u8, c_str, "n");
        } else if (mem.eql(u8, opcao, "3")) {
            // Modo de apenas palavras
            try stdout.print("\n  Numero de palavras: ", .{});
            const n_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse continue;
            const num = fmt.parseInt(u32, mem.trim(u8, n_raw, " \t\r\n"), 10) catch 20;

            var gerador = GeradorLorem.init(prng.random(), config);
            try stdout.print("\n", .{});
            var i: u32 = 0;
            while (i < num) : (i += 1) {
                if (i > 0) try stdout.print(" ", .{});
                try stdout.print("{s}", .{gerador.palavraAleatoria()});
            }
            try stdout.print("\n", .{});
            continue;
        } else if (mem.eql(u8, opcao, "4")) {
            try stdout.print("\n  Numero de frases: ", .{});
            const n_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse continue;
            const num = fmt.parseInt(u32, mem.trim(u8, n_raw, " \t\r\n"), 10) catch 5;

            var gerador = GeradorLorem.init(prng.random(), config);
            try stdout.print("\n", .{});
            var i: u32 = 0;
            while (i < num) : (i += 1) {
                try gerador.gerarFrase(stdout);
            }
            try stdout.print("\n", .{});
            continue;
        } else if (!mem.eql(u8, opcao, "1")) {
            try stdout.print("  Opcao invalida.\n", .{});
            continue;
        }

        config.validar() catch {
            try stdout.print("  Configuracao invalida. Usando valores padrao.\n", .{});
            config = ConfigGeracao{};
        };

        try stdout.print("\n--- Texto Gerado ---\n\n", .{});

        // Gera para um buffer para poder contar estatísticas
        var texto_buf = ArrayList(u8).init(std.heap.page_allocator);
        defer texto_buf.deinit();

        var gerador = GeradorLorem.init(prng.random(), config);
        try gerador.gerar(texto_buf.writer());

        // Exibe o texto
        try stdout.print("{s}\n", .{texto_buf.items});

        // Estatísticas
        const stats = EstatisticasTexto.analisar(texto_buf.items);
        try stdout.print(
            \\
            \\--- Estatisticas ---
            \\  Paragrafos: {d}
            \\  Frases:     {d}
            \\  Palavras:   {d}
            \\  Caracteres: {d}
            \\
        , .{ stats.paragrafos, stats.frases, stats.palavras, stats.caracteres });
    }

    try stdout.print("\n  Ate logo!\n", .{});
}

Passo 7: Testes

test "capitalizar palavra" {
    var buf: [64]u8 = undefined;
    const resultado = GeradorLorem.capitalizar("lorem", &buf);
    try std.testing.expectEqualStrings("Lorem", resultado);
}

test "capitalizar vazio" {
    var buf: [64]u8 = undefined;
    const resultado = GeradorLorem.capitalizar("", &buf);
    try std.testing.expectEqualStrings("", resultado);
}

test "estatisticas de texto" {
    const texto = "Primeira frase. Segunda frase.\n\nSegundo paragrafo.";
    const stats = EstatisticasTexto.analisar(texto);
    try std.testing.expectEqual(@as(usize, 3), stats.frases);
    try std.testing.expectEqual(@as(usize, 2), stats.paragrafos);
    try std.testing.expectEqual(@as(usize, 6), stats.palavras);
}

test "config padrao e valida" {
    const config = ConfigGeracao{};
    try config.validar();
}

test "config invalida - zero paragrafos" {
    var config = ConfigGeracao{};
    config.paragrafos = 0;
    try std.testing.expectError(error.ParagrafosInvalidos, config.validar());
}

test "config invalida - min > max frases" {
    var config = ConfigGeracao{};
    config.frases_por_paragrafo_min = 10;
    config.frases_por_paragrafo_max = 3;
    try std.testing.expectError(error.FrasesInvalidas, config.validar());
}

test "vocabulario nao esta vazio" {
    try std.testing.expect(vocabulario.len > 50);
}

Compilando e Executando

zig build test
zig build run

Conceitos Aprendidos

  • Arrays constantes de strings em comptime
  • Geração aleatória de índices para seleção de elementos
  • Writer interfaces para saída flexível
  • Validação de configuração com erros
  • ArrayList para construção de strings dinâmicas
  • Capitalização de caracteres ASCII

Próximos Passos

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.