Tbpgr Blog

Employee Experience Engineer tbpgr(てぃーびー) のブログ

RubyでInterpreterパターン/HTML5のセクション構造に対応したWebページの生成

概要

GoFデザインパターンInterpreterパターンについて。
任意の文法規則をクラスで表します。
汎用言語を使用して、特定分野の用途に絞ったミニ言語(DSL)を作成する際などに利用される。
問題領域に近く・処理効率がよく・保守性の高いプログラムの作成が可能となる。

登場人物

AbstractExpression = 命令
TerminalExpression = 具象命令(終端)
NonterminalExpression = 具象命令(終端以外)
Context = 状況、文脈
Client = 利用者

UML


実装サンプル

サンプル概要

以下のルールで記述されたプレーンテキストをもとに
HTML5のセクション構造に対応したWebページを生成する。
今回は再帰構造はなしにします。

・WebページはHeadとBodyを持ちます。Bodyはセクション要素を0個からn個持つことが出来ます。
・Headにはエンコードの設定、ページのタイトル、CSSのインクルードを持ちます。
・エンコード名、ページのタイトル、CSSのインクルード設定は任意のテキストで持ちます。
・セクション要素はSection,Article,Nav,Header,Footerのどれかです。
・Sectionタグであらわされる1つのセクションは開始タグ、属性、Sectionの中身。閉じタグを持ちます。
  属性は属性名と属性値のセットからなり、0個からn個持つことが出来ます。
  Sectionの中身は任意のテキストおよびHTMLのタグを0個からN個持ちます。
・テキストは任意の文字列です
・html-tagは任意のタグです。
・Article、Aside、Nav、Header、FooterはSectionと同じ構造です

▼出力ファイルのBNF定義

<html5-section-page> ::= "<!DOCTYPE HTML><html><page-head><body>" [<section-factor>{<section-factor>}] "</body></html>"
<page-head> ::= "<head>" <encode-type> <page-title> <css-include> "</head>"
<encode-type> ::= <meta charset=\"" <text> "\" />"
<css-include> ::= <link rel=\"stylesheet\" type=\"text/css\" href=\"" <text> ".css\" />
<page-title> ::= "<title>" <text> "</title>"
<section-factor> ::= <section> | <article> | <nav> | <aside> | <header> | <footer>
<section> ::= "<section" [<section-attribute>{attribute}] ">" <section-contents> "</section>"
<section-attribute> ::= " " <section-attribute-name> "=\"" <section-attribute-value> "\""
<section-contents> ::= <text> | <html-tag>
<article> ::= "<article" [<article-attribute>{attribute}] ">" <article-contents> "</article>"
<article-attribute> ::= " " <article-attribute-name> "=\"" <article-attribute-value> "\""
<nav> ::= "<nav" [<nav-attribute>{attribute}] ">" <nav-contents> "</nav>"
<nav-attribute> ::= " " <nav-attribute-name> "=\"" <nav-attribute-value> "\""
<nav-contents> ::= <text> | <html-tag>
<aside> ::= "<aside" [<aside-attribute>{attribute}] ">" <aside-contents> "</aside>"
<aside-attribute> ::= " " <aside-attribute-name> "=\"" <aside-attribute-value> "\""
<aside-contents> ::= <text> | <html-tag>
<header> ::= "<header" [<header-attribute>{attribute}] ">" <header-contents> "</header>"
<header-attribute> ::= " " <header-attribute-name> "=\"" <header-attribute-value> "\""
<header-contents> ::= <text> | <html-tag>
<footer> ::= "<footer" [<footer-attribute>{attribute}] ">" <footer-contents> "</footer>"
<footer-attribute> ::= " " <footer-attribute-name> "=\"" <footer-attribute-value> "\""
<footer-contents> ::= <text> | <html-tag>
<text> :== 任意の文字列
<html-tag> :== 任意のHTMLタグ

上記のHTML5のWebページを生成するにあたって、以下のミニ言語を定義します。

▼ミニ言語のBNF定義

<html5-section-page> ::= <encode-type> <page-title> <css-directory> <css-filename> [<section-factor>{<section-factor>}]
<encode-type> ::= "et=" <text> "\n"
<page-title> ::= "pt=" <text> "\n"
<css-directory> ::= "cd=" <text> ",cf=" <text> "\n"
<section-factor> ::= "sf=" <section-factor-type>" [<section-factor-attribute>{<section-factor-attribute>}] [", sfc=" <text> {", sfc=" <text>}]
<section-factor-type> ::= "section" | "article" | "nav" | "aside" | "header" | "footer"
<section-factor-attribute> ::= ",sfa=" <attribute-name> "<<" <attribute-value> "|"{<attribute-name> "<<" <attribute-value> "|"}
<attribute-name> ::= 任意の属性名
<attribute-value> ::= 任意の属性値
<section-factor-contents> ::= "sfc=" <text> | "{" <section-factor> "}" | <html-tag>
<text> :== 任意の文字列
登場人物

AbstractExpression = Html5GeneratorExpression : 命令
NonterminalExpression = Html5EncodeTypeExpression : エンコード設定。具象命令(終端以外)
NonterminalExpression = Html5HeadStartExpression : Headタグの開始。具象命令(終端以外)
NonterminalExpression = Html5HeadEndExpression : Headタグの終了。具象命令(終端以外)
NonterminalExpression = Html5BodyStartExpression : Bodyタグの開始。具象命令(終端以外)
NonterminalExpression = Html5BodyEndExpression : Bodyタグの終了。具象命令(終端以外)
NonterminalExpression = Html5PageTitleExpression : タイトル。具象命令(終端以外)
NonterminalExpression = Html5CssExpression : CSSファイル名とファイルパス。具象命令(終端以外)
NonterminalExpression = Html5SectionFactorExpression : セクション要素のタイプ、名前、値、コンテンツ(再帰可能)。具象命令(終端以外)
XXX = Html5Parser : 各命令を解析
Context = Html5Context : 状況、文脈。命令全体の保持
Client = main : 利用者

UML


サンプルコード

Html5GeneratorExpression

# encoding: Shift_JIS

=begin rdoc
= Html5GeneratorExpressionクラス
=end
class Html5GeneratorExpression
  NOT_OVERRIDE = 'not override error'
  attr_accessor :expression
  
  def initialize(expression)
    @expression = expression
  end
  
  def parse()
    raise NOT_OVERRIDE
  end
end

Html5EncodeTypeExpression

# encoding: Shift_JIS
require_relative './html5_generator_expression'

=begin rdoc
= Html5EncodeTypeExpressionクラス
=end
class Html5EncodeTypeExpression < Html5GeneratorExpression
  ENCODE_VALUE_INDEX = 1
  def parse()
    return "\t\t<meta http-equiv=\"Content-Type\" content=\"text/html;charset=#{@expression.split('=')[ENCODE_VALUE_INDEX]}\" />"
  end
end

Html5HeadStartExpression

# encoding: Shift_JIS
require_relative './html5_generator_expression'

=begin rdoc
= Html5HeadStartExpressionクラス
=end
class Html5HeadStartExpression < Html5GeneratorExpression
  def parse()
    return "\t<head>"
  end
end

Html5HeadEndExpression

# encoding: Shift_JIS
require_relative './html5_generator_expression'

=begin rdoc
= Html5HeadEndExpressionクラス
=end
class Html5HeadEndExpression < Html5GeneratorExpression
  def parse()
    return "\t<\/head>"
  end
end

Html5BodyStartExpression

# encoding: Shift_JIS
require_relative './html5_generator_expression'

=begin rdoc
= Html5BodyStartExpressionクラス
=end
class Html5BodyStartExpression < Html5GeneratorExpression
  def parse()
    return "\t<body>"
  end
end

Html5BodyEndExpression

# encoding: Shift_JIS
require_relative './html5_generator_expression'

=begin rdoc
= Html5BodyEndExpressionクラス
=end
class Html5BodyEndExpression < Html5GeneratorExpression
  def parse()
    return "\t<\/body>"
  end
end

Html5PageTitleExpression

# encoding: Shift_JIS
require_relative './html5_generator_expression'

=begin rdoc
= Html5PageTitleExpressionクラス
=end
class Html5PageTitleExpression < Html5GeneratorExpression
  TITLE_VALUE_INDEX = 1
  def parse()
    return "\t\t<title>#{@expression.split('=')[TITLE_VALUE_INDEX]}</title>"
  end
end

Html5CssExpression

# encoding: Shift_JIS
require_relative './html5_generator_expression'

=begin rdoc
= Html5CssExpressionクラス
=end
class Html5CssExpression < Html5GeneratorExpression
  CSS_FOLDER = 0
  CSS_FILENAME = 1
  CSS_VALUE_INDEX = 1
  def parse()
    css_attribute_list = @expression.split(',')
    css_folder = css_attribute_list[CSS_FOLDER].split('=')[CSS_VALUE_INDEX]
    css_filename = css_attribute_list[CSS_FILENAME].split('=')[CSS_VALUE_INDEX]
    return "\t\t<link rel=\"stylesheet\" type=\"text/css\" href=\"#{css_folder}#{css_filename}.css\" />"
  end
end

Html5SectionFactorExpression

# encoding: Shift_JIS
require_relative './html5_generator_expression'

=begin rdoc
= Html5SECTION_FACTORExpressionクラス
=end
class Html5SectionFactorExpression < Html5GeneratorExpression
  SECTION_FACTOR_TYPE = 0
  SECTION_FACTOR_ATTRIBUTE = 1
  SECTION_FACTOR_CONTENTS = 2
  SECTION_FACTOR_NAME_INDEX = 0
  SECTION_FACTOR_VALUE_INDEX = 1

  def parse()
    section_factor_attribute_list = @expression.split(',')
    section_factor_type = section_factor_attribute_list[SECTION_FACTOR_TYPE].split('=')[SECTION_FACTOR_VALUE_INDEX]
    section_factor_attribute_lists = section_factor_attribute_list[SECTION_FACTOR_ATTRIBUTE].split('=')[SECTION_FACTOR_VALUE_INDEX]
    section_factor_contents = section_factor_attribute_list[SECTION_FACTOR_CONTENTS].split('=')[SECTION_FACTOR_VALUE_INDEX]
    html=""
    html << "\t\t<#{section_factor_type}"
 
    section_factor_attribute_lists.split('|').each {|section_factor_attribute_list|
      attribute = section_factor_attribute_list.split('<<')
      html << " #{attribute[SECTION_FACTOR_NAME_INDEX]}='#{attribute[SECTION_FACTOR_VALUE_INDEX]}'"
    }
    html << ">"
    html << section_factor_contents
    html << "</#{section_factor_type}>"
    return html
  end
end

Html5Parser

# encoding: Shift_JIS
require_relative './html5_generator_expression'
require_relative './html5_encode_type_expression'
require_relative './html5_page_title_expression'
require_relative './html5_head_start_expression'
require_relative './html5_head_end_expression'
require_relative './html5_body_start_expression'
require_relative './html5_body_end_expression'
require_relative './html5_css_expression'
require_relative './html5_section_factor_expression'

=begin rdoc
= Html5Parserクラス
=end
class Html5Parser
  attr_accessor :expression_list
  # headタグ開始
  HS="hs"
  # headタグ終了
  HE="he"
  # エンコードタイプ
  ET="et"
  # ページタイトル
  PT="pt"
  # CSSディレクトリ
  CD="cd"
  # Bodyタグ開始
  BS="bs"
  # Bodyタグ終了
  BE="be"
  # セクション要素
  SF="sf"
  
  def initialize(expressions)
    @expression_list=Array.new
  
    expressions.split("\n").each {|expression|
      case expression.split('=')[0]
      when HS
        @expression_list.push Html5HeadStartExpression.new(expression)
      when ET
        @expression_list.push Html5EncodeTypeExpression.new(expression)
      when PT
        expression_list.push Html5PageTitleExpression.new(expression)
      when CD
        expression_list.push Html5CssExpression.new(expression)
      when HE
        @expression_list.push Html5HeadEndExpression.new(expression)
      when BS
        @expression_list.push Html5BodyStartExpression.new(expression)
      when SF
        expression_list.push Html5SectionFactorExpression.new(expression)
      when BE
        @expression_list.push Html5BodyEndExpression.new(expression)
      end
    }

  end
  
  def parse()
html= << "EOS"
<!DOCTYPE HTML>
<html lang="ja-JP">
EOS

    @expression_list.each {|expression|
      html << "\t\t"
      html << expression.parse
      html << "\n"
    }
    html << '</html>'
    return html
  end
end

Html5Context

# encoding: Shift_JIS
require_relative './html5_parser'

=begin rdoc
= Html5Contextクラス
=end
class Html5Context
  attr_accessor :expressions
  
  def initialize(expressions)
    @expressions = expressions
  end
  
  def parse()
    html5_parser = Html5Parser.new(@expressions)
    return html5_parser.parse
  end
end

main

# encoding: Shift_JIS
require_relative './html5_context'

input =<<"EOS"
hs
et=UTF-8
pt=hello html5
cd=./,cf=hello_html5
he
bs
sf=section,sfa=id<<section1|class<<section,sfc=section_contents1<br />section_contents2
sf=article,sfa=id<<article1|class<<article,sfc=article_contents
sf=nav,sfa=id<<nav1|class<<nav,sfc=nav_contents
sf=aside,sfa=id<<aside1|class<<aside,sfc=aside_contents
sf=header,sfa=id<<header1|class<<header,sfc=header_contents
sf=footer,sfa=id<<footer1|class<<footer,sfc=footer_contents
be
EOS

html5_context = Html5Context.new(input)
html = html5_context.parse
puts html

hello_html5.css

section.section {
  background-color:skyblue;
}
article.article {
  background-color:yellow;
}
nav.nav {
  background-color:green;
}
aside.aside {
  background-color:gray;
}

header.header {
  background-color:#115533;
}

footer.footer {
  background-color:#118844;
}
出力結果HTML
<!DOCTYPE HTML>
<html lang="ja-JP">
      <head>
        <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
        <title>hello html5</title>
        <link rel="stylesheet" type="text/css" href="./hello_html5.css" />
      </head>
      <body>
        <section id='section1' class='section'>section_contents1<br />section_contents2</section>
        <article id='article1' class='article'>article_contents</article>
        <nav id='nav1' class='nav'>nav_contents</nav>
        <aside id='aside1' class='aside'>aside_contents</aside>
        <header id='header1' class='header'>header_contents</header>
        <footer id='footer1' class='footer'>footer_contents</footer>
      </body>
</html>
出力結果画面キャプチャ