Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/redirect.git
<?php
final class PageGenerator {
  private const COMMON_WORDS = [
    'all', 'and', 'boy', 'car', 'cat', 'day', 'end', 'family', 'home', 'it',
    'man', 'name', 'one', 'people', 'read', 'school', 'speak', 'the', 'this',
    'you', 'ask', 'book', 'can', 'dog', 'eye', 'first', 'go', 'he', 'child',
    'in', 'learn', 'morning', 'open', 'play', 'question', 'room', 'say',
    'start', 'today', 'word', 'about', 'at', 'brother', 'drink', 'easy',
    'father', 'girl', 'help', 'chair', 'know', 'my', 'new', 'paper', 'please',
    'rich', 'she', 'show', 'son', 'they', 'what', 'always', 'be', 'body',
    'careful', 'cry', 'door', 'everything', 'face', 'her', 'if', 'many', 'no',
    'pen', 'place', 'road', 'stop', 'student', 'two', 'want', 'where',
    'answer', 'between', 'clear', 'country', 'dance', 'do', 'each', 'friend',
    'his', 'job', 'life', 'more', 'park', 'person', 'ready', 'second', 'soon',
    'that', 'we', 'why', 'able', 'before', 'but', 'clean', 'close', 'dream',
    'eight', 'for', 'hand', 'inside', 'now', 'or', 'picture', 'river', 'ship',
    'shop', 'sit', 'table', 'very', 'write', 'air', 'black', 'cinema',
    'daughter', 'eat', 'from', 'good', 'head', 'cheese', 'important', 'land',
    'money', 'pay', 'problem', 'run', 'same', 'see', 'send', 'thing', 'work',
    'any', 'as', 'better', 'cold', 'come', 'doctor', 'find', 'game', 'idea',
    'kind', 'live', 'make', 'peace', 'popular', 'right', 'small', 'so', 'some',
    'there', 'wait', 'again', 'back', 'could', 'document', 'egg', 'fire',
    'give', 'chance', 'information', 'light', 'may', 'often', 'prefer', 'put',
    'red', 'stone', 'such', 'think', 'understand', 'visit', 'around', 'best',
    'call', 'cut', 'dinner', 'down', 'explain', 'get', 'interesting', 'long',
    'move', 'out', 'page', 'reach', 'rest', 'set', 'should', 'stand', 'time',
    'up', 'age', 'because', 'big', 'camera', 'city', 'dress', 'evening',
    'free', 'have', 'ill', 'like', 'mother', 'old', 'police', 'remember',
    'street', 'study', 'teacher', 'voice', 'water', 'also', 'box', 'class',
    'difficult', 'drive', 'food', 'great', 'happy', 'change', 'juice', 'meet',
    'need', 'pretty', 'quite', 'real', 'sad', 'spring', 'star', 'take', 'yes',
    'action', 'alone', 'breakfast', 'continue', 'dead', 'enjoy', 'full',
    'garden', 'house', 'journey', 'much', 'nothing', 'phone', 'price',
    'result', 'sister', 'sun', 'tell', 'view', 'with', 'against', 'bus',
    'company', 'desert', 'expensive', 'flower', 'green', 'church',
    'impossible', 'leave', 'month', 'on', 'plan', 'possible', 'return', 'save',
    'sea', 'something', 'together', 'woman', 'anything', 'army', 'bad',
    'cover', 'culture', 'decision', 'example', 'feel', 'how', 'island',
    'member', 'next', 'position', 'present', 'record', 'sleep', 'sweet', 'try',
    'under', 'world', 'after', 'bed', 'buy', 'catch', 'corner', 'distance',
    'education', 'fast', 'here', 'interest', 'letter', 'never', 'part',
    'president', 'round', 'several', 'sound', 'story', 'talk', 'week',
    'almost', 'bread', 'control', 'dear', 'every', 'few', 'gold', 'chief',
    'invite', 'late', 'most', 'only', 'product', 'public', 'receive', 'sorry',
    'strong', 'then', 'too', 'way', 'across', 'art', 'bring', 'carry',
    'confirm', 'die', 'east', 'group', 'hope', 'industry', 'look', 'must',
    'own', 'personal', 'reason', 'service', 'shall', 'stay', 'their', 'wife',
    'away', 'beautiful', 'care', 'cost', 'deep', 'enough', 'fight', 'garage',
    'into', 'keep', 'miss', 'other', 'player', 'rather', 'remain', 'side',
    'south', 'true', 'use', 'who', 'already', 'become', 'cause', 'certain',
    'describe', 'dry', 'expect', 'fact', 'hard', 'include', 'let', 'moment',
    'power', 'provide', 'report', 'seat', 'single', 'system', 'through',
    'which', 'apple', 'blue', 'clock', 'colour', 'different', 'earth', 'film',
    'glad', 'hour', 'just', 'love', 'number', 'pencil', 'quick', 'rain',
    'simple', 'summer', 'town', 'tree', 'window', 'address', 'building',
    'computer', 'cross', 'desk', 'ear', 'fish', 'glass', 'ice', 'key',
    'minute', 'office', 'parent', 'post', 'rock', 'search', 'sport', 'tea',
    'valley', 'walk', 'airport', 'baby', 'card', 'central', 'direction',
    'dollar', 'fruit', 'gift', 'high', 'illness', 'milk', 'not', 'piece',
    'protect', 'race', 'since', 'slow', 'smile', 'ticket', 'well', 'accident',
    'blood', 'business', 'during', 'even', 'floor', 'general', 'choose',
    'inform', 'little', 'meeting', 'order', 'party', 'pink', 'reply', 'snow',
    'sugar', 'travel', 'virus', 'watch', 'another', 'believe', 'both', 'crazy',
    'cup', 'decide', 'ever', 'field', 'heart', 'imagine', 'line', 'meat',
    'over', 'pull', 'ring', 'sell', 'similar', 'speed', 'than', 'your',
    'above', 'begin', 'century', 'consider', 'dangerous', 'dark', 'exchange',
    'government', 'hear', 'jump', 'material', 'near', 'past', 'produce',
    'remove', 'secret', 'song', 'television', 'value', 'when', 'arm', 'behind',
    'case', 'collect', 'draw', 'examine', 'fall', 'grow', 'immediately', 'low',
    'mind', 'off', 'pass', 'radio', 'shoe', 'station', 'sure', 'test', 'usual',
    'while', 'ago', 'along', 'bear', 'condition', 'direct', 'edge', 'fine',
    'half', 'chicken', 'increase', 'magazine', 'nature', 'plate', 'poor',
    'respect', 'sharp', 'sometimes', 'still', 'tall', 'would', 'add', 'among',
    'built', 'common', 'depend', 'early', 'fly', 'happen', 'check',
    'introduce', 'less', 'mark', 'patient', 'perhaps', 'rise', 'sense',
    'short', 'state', 'turn', 'will', 'act', 'appear', 'break', 'course',
    'court', 'discuss', 'effect', 'form', 'hold', 'insect', 'mean', 'once',
    'purpose', 'really', 'ride', 'situation', 'success', 'though', 'upon',
    'war', 'afternoon', 'busy', 'coffee', 'detail', 'especially', 'finish',
    'ground', 'holiday', 'choice', 'kitchen', 'lesson', 'music', 'orange',
    'perfect', 'request', 'season', 'sick', 'tomorrow', 'welcome', 'yesterday',
    'agree', 'bridge', 'cake', 'customer', 'date', 'enter', 'future',
    'gentleman', 'hair', 'image', 'language', 'market', 'plane', 'private',
    'restaurant', 'size', 'sky', 'smart', 'thank', 'weather', 'actor',
    'bottle', 'cloth', 'coat', 'destroy', 'everywhere', 'finger', 'guide',
    'improve', 'knife', 'large', 'mistake', 'ocean', 'plant', 'repeat', 'salt',
    'special', 'teach', 'uncle', 'winter', 'angry', 'article', 'build',
    'dirty', 'except', 'famous', 'gas', 'hotel', 'cheap', 'interview', 'moon',
    'nice', 'prepare', 'prison', 'rice', 'seem', 'skirt', 'strange', 'train',
    'warm', 'account', 'bird', 'cloud', 'comfortable', 'damage', 'dust',
    'exercise', 'favourite', 'hospital', 'joke', 'message', 'night', 'paint',
    'pleasure', 'relationship', 'science', 'serious', 'spend', 'tired',
    'wrong', 'amount', 'bank', 'brown', 'crowd', 'deal', 'engine', 'follow',
    'chocolate', 'individual', 'left', 'meal', 'oil', 'pain', 'probably',
    'replace', 'society', 'square', 'step', 'temperature', 'university',
    'accept', 'advance', 'bag', 'captain', 'centre', 'demand', 'enemy',
    'factory', 'hungry', 'illegal', 'law', 'nose', 'petrol', 'proud',
    'responsible', 'store', 'successful', 'swim', 'top', 'win', 'available',
    'boat', 'borrow', 'coast', 'cream', 'design', 'expression', 'farm',
    'history', 'injure', 'map', 'obtain', 'peaceful', 'practise', 'recently',
    'shape', 'silver', 'smoke', 'touch', 'wash', 'advantage', 'attack',
    'butter', 'club', 'college', 'degree', 'escape', 'gate', 'independent',
    'listen', 'marry', 'object', 'path', 'quiet', 'refuse', 'subject',
    'supply', 'taste', 'usually', 'vegetable', 'arrange', 'below', 'cigarette',
    'cottage', 'department', 'earn', 'front', 'gentle', 'hat', 'instrument',
    'machine', 'newspaper', 'parcel', 'religion', 'repair', 'serve',
    'shoulder', 'trip', 'village', 'wall', 'arrive', 'born', 'clothes',
    'correct', 'double', 'english', 'forget', 'goal', 'hate', 'kill', 'last',
    'main', 'pair', 'promise', 'regular', 'somewhere', 'space', 'these',
    'useful', 'without', 'animal', 'beer', 'calm', 'copy', 'dish', 'express',
    'foreign', 'guess', 'husband', 'lie', 'mine', 'opinion', 'passenger',
    'press', 'rule', 'sign', 'support', 'those', 'wonderful', 'year', 'afraid',
    'board', 'circle', 'count', 'death', 'discover', 'funny', 'guest', 'horse',
    'lake', 'modern', 'necessary', 'plenty', 'profit', 'reduce', 'share',
    'steal', 'trust', 'wish', 'young', 'admire', 'allow', 'battle', 'climb',
    'complete', 'divide', 'effort', 'fresh', 'hole', 'indeed', 'marriage',
    'outside', 'pleasant', 'point', 'recent', 'secretary', 'sing', 'soft',
    'third', 'various', 'adventure', 'although', 'bottom', 'coin', 'comfort',
    'drop', 'equal', 'gun', 'intelligent', 'join', 'laugh', 'middle',
    'perform', 'plain', 'row', 'soldier', 'surface', 'thick', 'until', 'wild',
    'attempt', 'bill', 'breathe', 'cook', 'defend', 'fat', 'grey', 'hot',
    'character', 'import', 'lose', 'mountain', 'operation', 'prize', 'risk',
    'safe', 'suddenly', 'suit', 'type', 'wood', 'area', 'asleep', 'bath',
    'careless', 'delay', 'event', 'foot', 'hide', 'chain', 'international',
    'match', 'nervous', 'pity', 'prove', 'raise', 'shut', 'smell', 'straight',
    'trade', 'variety', 'admit', 'attend', 'branch', 'coal', 'consist',
    'declare', 'exact', 'farmer', 'instead', 'jacket', 'leg', 'metal',
    'opposite', 'pound', 'roll', 'score', 'shoot', 'speech', 'toilet', 'whose',
    'attitude', 'brave', 'contain', 'doubt', 'experience', 'flat', 'guard',
    'heavy', 'charge', 'iron', 'medicine', 'noise', 'politics', 'pour', 'rush',
    'smooth', 'spread', 'suggest', 'trouble', 'west', 'average', 'belong',
    'certainly', 'crime', 'duty', 'either', 'fail', 'health', 'influence',
    'leader', 'measure', 'offer', 'pile', 'regard', 'rough', 'series', 'spoil',
    'spot', 'thin', 'vote',
  ];

  private const ARTICLES  = ['the', 'this', 'that', 'some', 'many', 'all', 'each', 'no'];
  private const PRONOUNS  = ['he', 'she', 'it', 'they', 'we', 'you', 'who'];
  private const ADJECTIVES = [
    'big', 'small', 'red', 'blue', 'green', 'good', 'bad', 'happy',
    'sad', 'fast', 'slow', 'hot', 'cold', 'rich', 'poor', 'old',
  ];
  private const NOUNS = [
    'boy', 'girl', 'man', 'woman', 'child', 'dog', 'cat', 'car',
    'book', 'room', 'family', 'water', 'money', 'time', 'day',
  ];
  private const VERBS = [
    'eat', 'drink', 'play', 'read', 'speak', 'learn', 'say',
    'start', 'know', 'help', 'show', 'cry', 'stop', 'want',
  ];
  private const AUXILIARIES = ['will', 'can', 'must', 'may', 'should', 'could', 'would'];
  private const ADVERBS     = ['today', 'always', 'never', 'soon', 'now', 'often', 'again'];
  private const CONJUNCTIONS = ['and', 'but', 'or', 'so', 'because'];

  private const FONTS = [
    'sans-serif', 'serif', 'Georgia, serif', 'Verdana, sans-serif',
    '"Courier New", monospace', 'Tahoma, sans-serif',
  ];
  private const BACKGROUNDS = ['#ffffff', '#f9f9f9', '#f0f4f8', '#fff8f0', '#f5f5f5', '#fefce8'];
  private const FOREGROUNDS = ['#111', '#222', '#333', '#1a1a2e'];
  private const ACCENTS     = ['#0066cc', '#2a9d8f', '#e63946', '#6a4c93', '#457b9d', '#e76f51'];

  private const IMAGE_WIDTHS  = [150, 200, 300, 400, 600];
  private const IMAGE_HEIGHTS = [100, 150, 200, 300];

  public function generate(): string {
    $title    = $this->title();
    $sections = random_int( 2, 5 );

    $html  = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n"
      . "<meta charset=\"utf-8\">\n"
      . "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n"
      . '<title>' . self::esc( $title ) . "</title>\n"
      . '<style>' . $this->css() . "</style>\n"
      . "</head>\n<body>\n";

    if( random_int( 0, 1 ) ) {
      $html .= $this->nav();
    }

    $html .= '<h1>' . self::esc( $title ) . "</h1>\n"
      . '<p>' . $this->paragraph( 3, 5 ) . "</p>\n";

    for( $i = 0; $i < $sections; $i++ ) {
      $html .= $this->section() . "\n";
    }

    $html .= $this->footer() . "\n</body>\n</html>";

    return $html;
  }

  private static function esc( string $s ): string {
    return htmlspecialchars( $s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' );
  }

  private static function pick( array $pool ): string {
    return $pool[array_rand( $pool )];
  }

  private function words( int $min, int $max ): string {
    $count = random_int( $min, $max );
    $out   = '';

    for( $i = 0; $i < $count; $i++ ) {
      $out .= self::pick( self::COMMON_WORDS );

      if( $i < $count - 1 ) {
        $out .= ' ';
      }
    }

    return $out;
  }

  private function sentence(): string {
    $structs = [
      [self::ARTICLES, self::ADJECTIVES, self::NOUNS, self::AUXILIARIES,
       self::VERBS, self::ARTICLES, self::ADJECTIVES, self::NOUNS, self::ADVERBS],
      [self::PRONOUNS, self::AUXILIARIES, self::VERBS, self::ARTICLES,
       self::ADJECTIVES, self::NOUNS],
      [self::ARTICLES, self::ADJECTIVES, self::NOUNS, self::VERBS,
       self::NOUNS, self::ADVERBS],
      [self::PRONOUNS, self::VERBS, self::NOUNS, self::CONJUNCTIONS,
       self::ADJECTIVES, self::NOUNS],
      [self::ADVERBS, self::PRONOUNS, self::VERBS, self::ARTICLES, self::NOUNS],
    ];

    $pattern = self::pick( $structs );
    $out     = [];

    foreach( $pattern as $pool ) {
      $out[] = self::pick( $pool );
    }

    return ucfirst( implode( ' ', $out ) ) . '.';
  }

  private function paragraph( int $minSentences = 2, int $maxSentences = 6 ): string {
    $count     = random_int( $minSentences, $maxSentences );
    $sentences = [];

    for( $i = 0; $i < $count; $i++ ) {
      $sentences[] = $this->sentence();
    }

    return implode( ' ', $sentences );
  }

  private function title(): string {
    return ucwords( $this->words( 2, 6 ) );
  }

  private function heading( int $level = 2 ): string {
    $text = $this->title();
    return "<h{$level}>{$text}</h{$level}>";
  }

  private function itemList( bool $ordered = false ): string {
    $tag   = $ordered ? 'ol' : 'ul';
    $count = random_int( 3, 7 );
    $items = '';

    for( $i = 0; $i < $count; $i++ ) {
      $items .= '<li>' . ucfirst( $this->words( 2, 8 ) ) . '</li>';
    }

    return "<{$tag}>{$items}</{$tag}>";
  }

  private function table(): string {
    $cols = random_int( 2, 4 );
    $rows = random_int( 2, 5 );
    $html = '<table border="1" cellpadding="6" cellspacing="0"><tr>';

    for( $c = 0; $c < $cols; $c++ ) {
      $html .= '<th>' . ucfirst( self::pick( self::COMMON_WORDS ) ) . '</th>';
    }

    $html .= '</tr>';

    for( $r = 0; $r < $rows; $r++ ) {
      $html .= '<tr>';

      for( $c = 0; $c < $cols; $c++ ) {
        $html .= '<td>' . ucfirst( $this->words( 1, 3 ) ) . '</td>';
      }

      $html .= '</tr>';
    }

    $html .= '</table>';

    return $html;
  }

  private function blockquote(): string {
    return '<blockquote><p>' . $this->sentence() . '</p></blockquote>';
  }

  private function link(): string {
    $text = ucfirst( $this->words( 1, 4 ) );
    $slug = str_replace( ' ', '-', strtolower( $this->words( 2, 4 ) ) );

    return '<a href="/' . self::esc( $slug ) . '">' . self::esc( $text ) . '</a>';
  }

  private function nav(): string {
    $count = random_int( 3, 6 );
    $links = [];

    for( $i = 0; $i < $count; $i++ ) {
      $links[] = $this->link();
    }

    return '<nav>' . implode( ' | ', $links ) . '</nav><hr>';
  }

  private function imagePlaceholder(): string {
    $w   = self::pick( self::IMAGE_WIDTHS );
    $h   = self::pick( self::IMAGE_HEIGHTS );
    $alt = $this->words( 2, 5 );

    return '<p><img src="https://placehold.co/' . $w . 'x' . $h
      . '" alt="' . self::esc( $alt ) . '" width="' . $w . '" height="' . $h . '"></p>';
  }

  private function section(): string {
    $html  = $this->heading();
    $html .= '<p>' . $this->paragraph( 2, 4 ) . '</p>';
    $extra = random_int( 0, 5 );

    $html .= match( $extra ) {
      0 => $this->itemList( false ),
      1 => $this->itemList( true ),
      2 => $this->blockquote(),
      3 => $this->table(),
      4 => $this->imagePlaceholder(),
      5 => '',
    };

    return $html;
  }

  private function footer(): string {
    return '<hr><footer><p>&copy; ' . date( 'Y' ) . ' '
      . ucwords( $this->words( 2, 4 ) ) . '. '
      . ucfirst( $this->words( 3, 6 ) ) . '.</p></footer>';
  }

  private function css(): string {
    $font   = self::pick( self::FONTS );
    $bg     = self::pick( self::BACKGROUNDS );
    $fg     = self::pick( self::FOREGROUNDS );
    $accent = self::pick( self::ACCENTS );

    return "body{font-family:{$font};max-width:720px;margin:2rem auto;"
      . "padding:0 1rem;background:{$bg};color:{$fg};line-height:1.6}"
      . "h1,h2,h3{color:{$accent}}"
      . "a{color:{$accent}}"
      . "blockquote{border-left:4px solid {$accent};margin:1rem 0;"
      . "padding:.5rem 1rem;background:#f0f0f0}"
      . "table{border-collapse:collapse;margin:1rem 0}"
      . "th{background:#eee}"
      . "nav{padding:.5rem 0}"
      . "footer{color:#666;font-size:.85rem}"
      . "img{max-width:100%;height:auto}";
  }
}