OSDN Git Service

refactoring EntityConverter
[jindolf/JinParser.git] / src / main / java / jp / osdn / jindolf / parser / EntityConverter.java
index d193cb0..2951d10 100644 (file)
@@ -8,6 +8,7 @@
 package jp.osdn.jindolf.parser;
 
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -20,7 +21,7 @@ import jp.osdn.jindolf.parser.content.DecodedContent;
  * <p>文字実体参照は{@code &gt; &lt; &quot; &amp;}が対象。
  *
  * <p>U+005C(バックスラッシュ)をU+00A5(円通貨)に直す処理も行われる。
- * ※ 人狼BBSはShift_JIS(⊃JISX0201)で運営されているので、
+ * ※ 人狼BBS(F国以前)はShift_JIS(⊃JISX0201)で運営されているので、
  * バックスラッシュは登場しないはず。
  * ※ が、バックスラッシュを生成するShift_JISデコーダは存在する。
  *
@@ -43,33 +44,20 @@ public class EntityConverter{
 
     private static final String UCS4_PATTERN = "[\\x{10000}-\\x{10ffff}]";
 
-    private static final RegexCnv GT   = new RegexCnv("&gt;",       ">");
-    private static final RegexCnv LT   = new RegexCnv("&lt;",       "<");
-    private static final RegexCnv AMP  = new RegexCnv("&amp;",      "&");
-    private static final RegexCnv QUAT = new RegexCnv("&quot;",     DQ_STR);
-    private static final RegexCnv BS   = new RegexCnv(BS_PATTERN,   YEN_STR);
-    private static final RegexCnv UCS4 = new RegexCnv(UCS4_PATTERN, "?");
-
-    private static final List<RegexCnv> REGS =
-            Arrays.asList(GT, LT, AMP, QUAT, BS, UCS4);
-
-    private static final Pattern ORPAT;
-
-    static{
-        int groupNo = 1;
-        StringBuilder orRegex = new StringBuilder();
-        for(RegexCnv cnv : REGS){
-            if(groupNo > 1) orRegex.append('|');
-            orRegex.append('(');
-            orRegex.append(cnv.getRegex());
-            orRegex.append(')');
-            cnv.setGroupNo(groupNo++);
-        }
-        ORPAT = Pattern.compile(orRegex.toString());
-    }
+    private static final RepInfo GT   = new RepInfo("&gt;",       ">");
+    private static final RepInfo LT   = new RepInfo("&lt;",       "<");
+    private static final RepInfo AMP  = new RepInfo("&amp;",      "&");
+    private static final RepInfo QUAT = new RepInfo("&quot;",     DQ_STR);
+    private static final RepInfo BS   = new RepInfo(BS_PATTERN,   YEN_STR);
+    private static final RepInfo UCS4 = new RepInfo(UCS4_PATTERN, "?");
+
 
+    private final MultiMatcher multiMatcher = new MultiMatcher();
+
+    {
+        this.multiMatcher.putRepInfo(GT, LT, AMP, QUAT, BS, UCS4);
+    }
 
-    private final Matcher matcher;
     private final boolean replaceSmp;
 
 
@@ -90,25 +78,13 @@ public class EntityConverter{
      */
     public EntityConverter(boolean replaceSmp){
         super();
-        this.matcher = buildMatcher();
         this.replaceSmp = replaceSmp;
         return;
     }
 
 
     /**
-     * マッチャを生成する。
-     *
-     * @return マッチャ
-     */
-    private static Matcher buildMatcher(){
-        Matcher result = ORPAT.matcher("");
-        return result;
-    }
-
-
-    /**
-     * 実体参照の変換を行う。
+     * XHTML文字実体参照の変換を行う。
      *
      * @param srcContent 変換元文書
      * @return 切り出された変換済み文書
@@ -120,7 +96,7 @@ public class EntityConverter{
     }
 
     /**
-     * 実体参照の変換を行う。
+     * XHTML文字実体参照の変換を行う。
      *
      * @param srcContent 変換元文書
      * @param range 範囲指定
@@ -135,7 +111,7 @@ public class EntityConverter{
     }
 
     /**
-     * 実体参照の変換を行う。
+     * XHTML文字実体参照の変換を行う。
      *
      * @param srcContent 変換元文書
      * @param startPos 開始位置
@@ -150,11 +126,11 @@ public class EntityConverter{
     }
 
     /**
-     * 実体参照の変換を行い既存のDecodedContentに追加を行う。
+     * XHTML文字実体参照の変換を行い既存のDecodedContentに追加を行う。
      *
      * @param dstContent 追加先文書。nullなら新たな文書が用意される。
      * @param srcContent 変換元文書
-     * @return targetもしくは新規に用意された文書
+     * @return dstContentもしくは新規に用意された文書
      * @throws IndexOutOfBoundsException 位置指定に不正があった
      */
     public DecodedContent append(DecodedContent dstContent,
@@ -166,12 +142,12 @@ public class EntityConverter{
     }
 
     /**
-     * 実体参照の変換を行い既存のDecodedContentに追加を行う。
+     * XHTML文字実体参照の変換を行い既存のDecodedContentに追加を行う。
      *
      * @param dstContent 追加先文書。nullなら新たな文書が用意される。
      * @param srcContent 変換元文書
      * @param range 範囲指定
-     * @return targetもしくは新規に用意された文書
+     * @return dstContentもしくは新規に用意された文書
      * @throws IndexOutOfBoundsException 位置指定に不正があった
      */
     public DecodedContent append(DecodedContent dstContent,
@@ -184,13 +160,13 @@ public class EntityConverter{
     }
 
     /**
-     * 実体参照の変換を行い既存のDecodedContentに追加を行う。
+     * XHTML文字実体参照の変換を行い既存のDecodedContentに追加を行う。
      *
      * @param dstContent 追加先文書。nullなら新たな文書が用意される。
      * @param srcContent 変換元文書
      * @param startPos 開始位置
      * @param endPos 終了位置
-     * @return targetもしくは新規に用意された文書
+     * @return dstContentもしくは新規に用意された文書
      * @throws IndexOutOfBoundsException 位置指定に不正があった
      */
     public DecodedContent append(DecodedContent dstContent,
@@ -211,59 +187,184 @@ public class EntityConverter{
             result = dstContent;
         }
 
-        this.matcher.reset(srcContent.getRawContent());
-        this.matcher.region(startPos, endPos);
+        CharSequence rawContent = srcContent.getRawContent();
+        this.multiMatcher.setText(rawContent, startPos, endPos);
 
         int copiedPos = startPos;
-        while(this.matcher.find()){
-            int group = -1;
-            int matchStart = -1;
-            String altTxt = "";
-            for(RegexCnv rc : REGS){
-                group = rc.getGroupNo();
-                matchStart = this.matcher.start(group);
-                if(matchStart >= 0){
-                    if(rc == UCS4 &&  ! this.replaceSmp){
-                        altTxt = this.matcher.group(group);
-                    }else{
-                        altTxt = rc.getAltTxt();
-                    }
-                    break;
-                }
+
+        for(;;){
+            RepInfo repInfo = this.multiMatcher.multiFind();
+            if(repInfo == null) break;
+            if(repInfo == UCS4 &&  ! this.replaceSmp){
+                continue;
             }
-            assert group >= 1;
-            int matchEnd = this.matcher.end(group);
 
+            int matchStart = this.multiMatcher.getMatchStart();
+            int matchEnd   = this.multiMatcher.getMatchEnd();
             result.append(srcContent, copiedPos, matchStart);
+
+            String altTxt = repInfo.getAltTxt();
             result.append(altTxt);
 
             copiedPos = matchEnd;
         }
-        result.append(srcContent, copiedPos, endPos);
 
-        this.matcher.reset("");
+        result.append(srcContent, copiedPos, endPos);
 
         return result;
     }
 
 
     /**
-     * 文字列置換リスト
+     * 同時に複数の正規表現をOR探索するマッチャ
      */
-    private static class RegexCnv{
+    private static class MultiMatcher{
+
+        private static final char REGEX_OR       = '|';
+        private static final char REGEX_GRPOPEN  = '(';
+        private static final char REGEX_GRPCLOSE = ')';
+
+
+        private List<RepInfo> repInfoList;
+        private Pattern orPattern;
+
+        private Matcher matcher;
+
+        private int matchStart = -1;
+        private int matchEnd   = -1;
+
+
+        /**
+         * コンストラクタ。
+         */
+        MultiMatcher(){
+            super();
+            return;
+        }
+
+
+        /**
+         * 置換情報を設定する。
+         *
+         * <p>先頭の置換情報の方が優先的にマッチングされる。
+         *
+         * @param infos 置換情報並び
+         */
+        void putRepInfo(RepInfo... infos){
+            List<RepInfo> list;
+            list = Arrays.asList(infos);
+            list = Collections.unmodifiableList(list);
+            this.repInfoList = list;
+
+            StringBuilder orRegex = new StringBuilder();
+            for(RepInfo repInfo : this.repInfoList){
+                String regex = repInfo.getRegex();
+
+                if(orRegex.length() != 0) orRegex.append(REGEX_OR);
+                orRegex.append(REGEX_GRPOPEN);
+                orRegex.append(regex);
+                orRegex.append(REGEX_GRPCLOSE);
+            }
+            this.orPattern = Pattern.compile(orRegex.toString());
+
+            this.matcher = this.orPattern.matcher("");
+            this.matchStart = -1;
+            this.matchEnd   = -1;
+
+            return;
+        }
+
+        /**
+         * 走査対象を設定する。
+         *
+         * @param seq 対象文字列
+         * @param startPos 走査開始位置
+         * @param endPos 走査終了位置
+         * @throws IllegalStateException 置換情報が未設定
+         */
+        void setText(CharSequence seq, int startPos, int endPos){
+            if(this.matcher == null) throw new IllegalStateException();
+
+            this.matcher.reset(seq);
+            this.matcher.region(startPos, endPos);
+
+            this.matchStart = -1;
+            this.matchEnd   = -1;
+
+            return;
+        }
+
+        /**
+         * マッチ開始位置を返す。
+         *
+         * @return 開始位置
+         */
+        int getMatchStart(){
+            return this.matchStart;
+        }
+
+        /**
+         * マッチ終了位置を返す。
+         *
+         * @return 終了位置
+         */
+        int getMatchEnd(){
+            return this.matchEnd;
+        }
+
+        /**
+         * 同時に複数の正規表現とのマッチングを試みるための走査を行う。
+         *
+         * <p>マッチングに伴いマッチ開始位置と終了位置が更新される。
+         *
+         * @return 最初にマッチングした正規表現。
+         *     マッチングしなければnullを返す。
+         * @throws IllegalStateException 置換情報が未設定
+         */
+        RepInfo multiFind(){
+            if(this.repInfoList == null || this.matcher == null){
+                throw new IllegalStateException();
+            }
+
+            if( ! this.matcher.find()) return null;
+
+            RepInfo result = null;
+
+            int group = 1;
+            for(RepInfo rc : this.repInfoList){
+                this.matchStart = this.matcher.start(group);
+                this.matchEnd   = this.matcher.end(group);
+                if(this.matchStart >= 0){
+                    result = rc;
+                    break;
+                }
+                group++;
+            }
+
+            return result;
+        }
+
+    }
+
+
+    /**
+     * 文字列置換設定。
+     */
+    private static class RepInfo{
 
         private final String regex;
         private final String altTxt;
-        private int groupNo;
 
 
         /**
          * コンストラクタ。
          *
+         * <p>正規表現文字列に前方参照グループ記号()を含めてはならない。
+         *
          * @param regex 置換元パターン正規表現
          * @param altTxt 置換文字列。
          */
-        RegexCnv(String regex, String altTxt){
+        RepInfo(String regex, String altTxt){
             this.regex = regex;
             this.altTxt = altTxt;
             return;
@@ -288,20 +389,6 @@ public class EntityConverter{
             return this.altTxt;
         }
 
-        /**
-         * パターン内において占めるグループ番号を返す。
-         *
-         * @return グループ番号
-         */
-        int getGroupNo(){
-            return this.groupNo;
-        }
-
-        void setGroupNo(int groupNo){
-            this.groupNo = groupNo;
-            return;
-        }
-
     }
 
 }