ツリー構造を可視化する

# DBに格納されている親子関係の情報からツリー構造で可視化する

- http://asistobe851.web.fc2.com/my-memo/traversal.html

大昔上記に書いていたものを転写した。しかもDBはMS ACCESS（笑）

```
$ ./desc.sh node
  1: nodeId               COUNTER                0         10
  2: isCategory           BIT                    0          1
  3: parentNodeId         INTEGER                0         10
  4: title                VARCHAR                0         50
  5: URI                  VARCHAR                0         50
  6: linkNodeId           INTEGER                0         10
  7: sequenceNo           INTEGER                0         10


$ java Dumper node
5       true    1       暮らし          NULL    0
6       true    1       政治            NULL    0
7       true    1       国際            NULL    0
8       true    1       文化・芸能              NULL    0
9       false   4       ブルーレイ方式で統一困難、東芝陣営確認　次世代ＤＶＤ            NULL    0
10      false   7       スマトラ沖、地震で約１０の新たな島　国土地理院が解析            NULL    0
15      false   3       山本が決勝Ｒへ　アーチェリー韓国国際競技大会            0       0
16      false   3       松井秀、イチローともに１安打　試合はヤンキース勝つ              0       0
17      false   3       Ｗ杯招致のラグビー協会、開催候補の９会場を発表          0       0
19      false   7       「避難民への銃撃見た」　ウズベク国境、住民ら恐怖語る            0       0
20      false   1       オリジナルカテゴリ              0       0
21      true    20      技術情報                0       0
22      true    21      JAVA            0       0
23      true    21      PERL            0       0
24      true    21      PHP             0       0
25      false   22      オブジェクトのシリアライズ・デシリアライズ              0       0
26      false   5       「高くても国産の農産物買う」消費者の８割　農業白書              0       0
27      false   2       紀宮さま結婚式、１１月１５日に帝国ホテルで              0       0
1       true    NULL    ルートでんねん          NULL    1
2       true    1       社会            0       0
3       true    1       スポーツ                NULL    0
4       true    1       ビジネス                NULL    0

$ java Traverser
ルートノードIDは、1 です。
子ノードを持っているものは以下の通りです。
1
        2       3       4       5       6       7       8       20
2
        27
3
        15      16      17
4
        9
5
        26
7
        10      19
20
        21
21
        22      23      24
22
        25

(1)ルートでんねん
├(2)社会
│└(27)紀宮さま結婚式、１１月１５日に帝国ホテルで
├(3)スポーツ
│├(15)山本が決勝Ｒへ　アーチェリー韓国国際競技大会
│├(16)松井秀、イチローともに１安打　試合はヤンキース勝つ
│└(17)Ｗ杯招致のラグビー協会、開催候補の９会場を発表
├(4)ビジネス
│└(9)ブルーレイ方式で統一困難、東芝陣営確認　次世代ＤＶＤ
├(5)暮らし
│└(26)「高くても国産の農産物買う」消費者の８割　農業白書
├(6)政治
├(7)国際
│├(10)スマトラ沖、地震で約１０の新たな島　国土地理院が解析
│└(19)「避難民への銃撃見た」　ウズベク国境、住民ら恐怖語る
├(8)文化・芸能
└(20)オリジナルカテゴリ
　└(21)技術情報
　　├(22)JAVA
　　│└(25)オブジェクトのシリアライズ・デシリアライズ
　　├(23)PERL
　　└(24)PHP





::::::::::::::
desc.sh
::::::::::::::
#!/bin/sh 
# $Id: traversal.html,v 1.1 2009/06/22 16:12:31 kishi Exp kishi $

if [ $# != 1 ] ; then
	echo "Usage: $0 [tablename]"
	exit 1
fi
TABLENAME=$1

java TableDescriptionRetriever $TABLENAME
::::::::::::::
Dumper.java
::::::::::::::
import java.util.*;
/**
$Id: traversal.html,v 1.1 2009/06/22 16:12:31 kishi Exp kishi $
*/

public class Dumper extends AbstractDataBaseAccessor {

    private String tableName;

    public Dumper( String tableName ) {
        super();

        this.tableName = tableName;
    }

    private void getResultList() {
        try {
            this.setSQL( "select * from " + tableName );

            List list = this.executeQuery();
            Iterator iterator = list.iterator();

            while ( iterator.hasNext() ) {
                Object[] record = ( Object[] ) iterator.next();

                for ( int i = 0;i < record.length;i++ ) {
                    if ( record[ i ] != null ) {
                        System.out.print( record[ i ].toString() );
                    } else {
                        System.out.print( "NULL" );
                    }
                    System.out.print( "\t" );
                }
                System.out.println();
            }


        } catch ( Exception e ) {
            e.printStackTrace();
        }
    }

    public static void main( String[] args ) {
        if ( args.length != 1 ) {
            System.out.println( "Usage: java Dumper [tablename]" );
            System.exit( 1 );
        }
        Dumper dumper = new Dumper( args[ 0 ] );
        dumper.getResultList();
    }
}
::::::::::::::
TableDescriptionRetriever.java
::::::::::::::
import java.sql.*;
import java.util.*;

/*
* $Id: traversal.html,v 1.1 2009/06/22 16:12:31 kishi Exp kishi $ 
* JDBC⇒ODBC経由でMSACCESSにアクセスしてみる
*/

public class TableDescriptionRetriever {
    static private String url = "jdbc:odbc:yacms";	// yacmsはマシン内のシステムDSNです

    public TableDescriptionRetriever( String tableName ) {

        try {
            Class.forName( "sun.jdbc.odbc.JdbcOdbcDriver" );
        } catch ( Exception e ) {
            e.printStackTrace();
        }

        try {
            Connection conn = DriverManager.getConnection( url, "admin", "admin" );

            String query = "select * from " + tableName;
            Statement stmt = conn.createStatement();

            ResultSet rs = stmt.executeQuery( query );

            // メタデータの取得
            ResultSetMetaData rsmd = rs.getMetaData();

            int numCols = rsmd.getColumnCount();
            for ( int i = 1; i <= numCols; i++ ) { // なぜか１から始まる
                String columnName = rsmd.getColumnName( i );
                String columnTypeName = rsmd.getColumnTypeName( i );
                int precision = rsmd.getPrecision( i );
                int scale = rsmd.getScale( i );
                System.out.printf( "%3d: %-20s %-20s %3d %10d\n",
                                   i , columnName, columnTypeName, scale, precision );
            }

            stmt.close();
            conn.close();

        } catch ( Exception e ) {
            e.printStackTrace();
        }
    }

    static public void main( String args[] ) {
        if ( args.length != 1 ) {
            System.err.println( "Usage: java TableDescriptionRetriever [tablename]" );
            System.exit( 1 );
        }
        TableDescriptionRetriever retriever = new TableDescriptionRetriever( args[ 0 ] );
    }
}

/*
$ javap java.sql.ResultSetMetaData
Compiled from "ResultSetMetaData.java"
public interface java.sql.ResultSetMetaData{
    public static final int columnNoNulls;
    public static final int columnNullable;
    public static final int columnNullableUnknown;
    public abstract int getColumnCount()       throws java.sql.SQLException;
    public abstract boolean isAutoIncrement(int)       throws java.sql.SQLException;
    public abstract boolean isCaseSensitive(int)       throws java.sql.SQLException;
    public abstract boolean isSearchable(int)       throws java.sql.SQLException;
    public abstract boolean isCurrency(int)       throws java.sql.SQLException;
    public abstract int isNullable(int)       throws java.sql.SQLException;
    public abstract boolean isSigned(int)       throws java.sql.SQLException;
    public abstract int getColumnDisplaySize(int)       throws java.sql.SQLException;
    public abstract java.lang.String getColumnLabel(int)       throws java.sql.SQLException;
    public abstract java.lang.String getColumnName(int)       throws java.sql.SQLException;
    public abstract java.lang.String getSchemaName(int)       throws java.sql.SQLException;
    public abstract int getPrecision(int)       throws java.sql.SQLException;
    public abstract int getScale(int)       throws java.sql.SQLException;
    public abstract java.lang.String getTableName(int)       throws java.sql.SQLException;
    public abstract java.lang.String getCatalogName(int)       throws java.sql.SQLException;
    public abstract int getColumnType(int)       throws java.sql.SQLException;
    public abstract java.lang.String getColumnTypeName(int)       throws java.sql.SQLException;
    public abstract boolean isReadOnly(int)       throws java.sql.SQLException;
    public abstract boolean isWritable(int)       throws java.sql.SQLException;
    public abstract boolean isDefinitelyWritable(int)       throws java.sql.SQLException;
    public abstract java.lang.String getColumnClassName(int)       throws java.sql.SQLException;
}
 
*/
::::::::::::::
AbstractDataBaseAccessor.java
::::::::::::::
import java.io.*;
import java.util.*;
import java.sql.*;

/**
$Id: traversal.html,v 1.1 2009/06/22 16:12:31 kishi Exp kishi $
データベースをアクセスするための抽象クラス
のはずだったが、全てのメソッドが実装済み（後でabstractメソッドが必要になるかもしれないのでとりあえずabstractなクラスにしておく)
MS-ACCESSのVARCHAR型はどっちみち５０バイトしか格納できないので本格版は別途対応
*/

abstract public class AbstractDataBaseAccessor {

    /** XMLで記述されたプロパティファイル */
    private final String PROP_XML = "../conf/prop.xml";

    private Properties prop;

    /** データベースへのコネクション(サブクラスでも使用するのでprotected修飾子にしておく) */
    protected java.sql.Connection connection = null;

    /** ステートメント */
    protected java.sql.PreparedStatement statement;

    public AbstractDataBaseAccessor() {
        init();
    }

    // データベースの接続設定のメソッドを実装しておく
    public void init() {

        String JDBC_DRIVER = null;
        String JDBC_URL = null;
        String USERID = null;
        String PASSWORD = null;

        prop = new Properties();
        try {
            InputStream stream = new FileInputStream( PROP_XML );
            prop.loadFromXML( stream );
            stream.close();

            JDBC_DRIVER = prop.getProperty( "JDBC_DRIVER" );
            JDBC_URL = prop.getProperty( "JDBC_URL" );
            USERID = prop.getProperty( "USERID" );
            PASSWORD = prop.getProperty( "PASSWORD" );

        } catch ( IOException e ) {
            e.printStackTrace();
        }

        // コネクションを取得
        try {
            Class.forName( JDBC_DRIVER );
            connection = DriverManager.getConnection( JDBC_URL, USERID, PASSWORD );
        } catch ( Exception e ) {
            e.printStackTrace();
        }

        //-------------------------------------------------------------------
        // 設定ファイルを読み取りコネクションを取得するところまで実装する
        //-------------------------------------------------------------------
    }

    public void dumpProperties() {
        // 一覧を出力
        prop.list( System.out );
    }

    public void setParam( int index, Object param ) throws Exception {
        try {
            statement.setObject( index, param );
        } catch ( Exception e ) {
            throw e;
        }
    }

    /** VARCHAR型の対応 */
    /*
        public void setCharacterStream( int index, String str ) throws Exception {
            try {
                StringReader reader = new StringReader( str );
                statement.setCharacterStream( index, reader, str.length() );
     
            } catch ( Exception e ) {
                throw e;
            }
        }
    */

    public void setSQL( String sql ) throws Exception {
        // SQLに引数を与えて実行する場合は、原則PreparedStatementを使うこと！→SQLインジェクション対応
        try {
            statement = connection.prepareStatement( sql );
        } catch ( Exception e ) {
            throw e;
        }
    }

    /** DB照会 */
    public List executeQuery() throws Exception {

        List resultList = new LinkedList();

        try {
            ResultSet resultSet = statement.executeQuery();

            ResultSetMetaData rsmd = resultSet.getMetaData();
            int numCols = rsmd.getColumnCount();
            // System.out.println( "カラム数＝" + numCols );

            // 各カラムの型を取得する(ここはMSアクセスのための対応 -- 他のDBでは必要ないはずである)
            String[] columnTypeNames = new String[ numCols ];
            for ( int i = 0;i < numCols;i++ ) {
                columnTypeNames[ i ] = rsmd.getColumnTypeName( i + 1 );
                // System.out.println( columnTypeNames[i] );
            }

            while ( resultSet.next() ) {

                Object[] cols = new Object[ numCols ];

                for ( int i = 0;i < numCols;i++ ) {

                    if ( "VARCHAR".equals( columnTypeNames[ i ] ) ) {

                        //==========================================================================
                        // ちなみにLONGCHAR型の場合はgetObject()でデータ取得が可能のようである
                        //==========================================================================
                        BufferedReader reader = new BufferedReader( resultSet.getCharacterStream( i + 1 ) );
                        StringBuilder sb = new StringBuilder();

                        ////////////////////////////////////////////////////////////
                        // int c;
                        // while ( ( c = reader.read() ) != -1 ) {
                        //     sb.append( ( char ) c );
                        // }
                        ////////////////////////////////////////////////////////////

                        String line = null;
                        while ( ( line = reader.readLine() ) != null ) {
                            sb.append( line + "\n" );
                        }

                        cols[ i ] = ( sb.toString() ).trim();

                    } else {
                        cols[ i ] = resultSet.getObject( i + 1 );
                    }
                }

                resultList.add( cols );
            }

        } catch ( Exception e ) {
            throw e;
        }

        return resultList;
    }

    /** DB更新 */
    public int executeUpdate() throws Exception {

        int resultCode = 0;

        try {
            connection.setAutoCommit( false );

            resultCode = statement.executeUpdate();

            connection.commit();

        } catch ( Exception e ) {
            throw e;
        }

        return resultCode;
    }

    /** お掃除処理 */
    public void cleanup() throws Exception {
        try {
            if ( statement != null ) {
                statement.close();
                statement = null;
            }
            if ( connection != null ) {
                connection.close();
                connection = null;
            }

        } catch ( Exception e ) {
            throw e;
        }

    }
}

/*
 
$ javap sun.jdbc.odbc.JdbcOdbcPreparedStatement
Compiled from "JdbcOdbcPreparedStatement.java"
public class sun.jdbc.odbc.JdbcOdbcPreparedStatement extends sun.jdbc.odbc.JdbcOdbcStatement implements java.sql.PreparedStatement{
    protected int numParams;
    protected sun.jdbc.odbc.JdbcOdbcBoundParam[] boundParams;
    protected sun.jdbc.odbc.JdbcOdbcBoundArrayOfParams arrayParams;
    protected java.util.Vector batchSqlVec;
    protected boolean batchSupport;
    protected boolean batchParamsOn;
    protected int batchSize;
    protected int arrayDef;
    protected int arrayScale;
    protected int StringDef;
    protected int NumberDef;
    protected int NumberScale;
    protected int batchRCFlag;
    protected int[] paramsProcessed;
    protected int[] paramStatusArray;
    protected long[] pA1;
    protected long[] pA2;
    protected int binaryPrec;
    protected sun.jdbc.odbc.JdbcOdbcUtils utils;
    public sun.jdbc.odbc.JdbcOdbcPreparedStatement(sun.jdbc.odbc.JdbcOdbcConnectionInterface);
    public void initialize(sun.jdbc.odbc.JdbcOdbc, long, long, java.util.Hashtable, int, int)       throws java.sql.SQLException;
    public java.sql.ResultSet executeQuery()       throws java.sql.SQLException;
    public java.sql.ResultSet executeQuery(java.lang.String)       throws java.sql.SQLException;
    public int executeUpdate()       throws java.sql.SQLException;
    public int executeUpdate(java.lang.String)       throws java.sql.SQLException;
    public boolean execute(java.lang.String)       throws java.sql.SQLException;
    public synchronized boolean execute()       throws java.sql.SQLException;
    public void setNull(int, int)       throws java.sql.SQLException;
    public void setBoolean(int, boolean)       throws java.sql.SQLException;
    public void setByte(int, byte)       throws java.sql.SQLException;
    public void setShort(int, short)       throws java.sql.SQLException;
    public void setInt(int, int)       throws java.sql.SQLException;
    public void setLong(int, long)       throws java.sql.SQLException;
    public void setReal(int, float)       throws java.sql.SQLException;
    public void setFloat(int, float)       throws java.sql.SQLException;
    public void setDouble(int, double)       throws java.sql.SQLException;
    public void setBigDecimal(int, java.math.BigDecimal)       throws java.sql.SQLException;
    public void setDecimal(int, java.math.BigDecimal)       throws java.sql.SQLException;
    public void setString(int, java.lang.String)       throws java.sql.SQLException;
    public void setBytes(int, byte[])       throws java.sql.SQLException;
    public void setDate(int, java.sql.Date)       throws java.sql.SQLException;
    public void setTime(int, java.sql.Time)       throws java.sql.SQLException;
    public void setTimestamp(int, java.sql.Timestamp)       throws java.sql.SQLException;
    public void setAsciiStream(int, java.io.InputStream, int)       throws java.sql.SQLException;
    public void setUnicodeStream(int, java.io.InputStream, int)       throws java.sql.SQLException;
    public void setBinaryStream(int, java.io.InputStream, int)       throws java.sql.SQLException;
    public void clearParameters()       throws java.sql.SQLException;
    public void clearParameter(int)       throws java.sql.SQLException;
    public void setObject(int, java.lang.Object)       throws java.sql.SQLException;
    public void setObject(int, java.lang.Object, int)       throws java.sql.SQLException;
    public void setObject(int, java.lang.Object, int, int)       throws java.sql.SQLException;
    public void addBatch(java.lang.String)       throws java.sql.SQLException;
    public void clearBatch();
    public void addBatch()       throws java.sql.SQLException;
    public int[] executeBatchUpdate()       throws java.sql.BatchUpdateException;
    protected int[] executeNoParametersBatch()       throws java.sql.BatchUpdateException;
    protected int getStmtParameterAttr(int)       throws java.sql.SQLException;
    protected void setStmtParameterSize(int)       throws java.sql.SQLException;
    protected void bindArrayOfParameters(int, int, int, int, java.lang.Object[], int[])       throws java.sql.SQLException;
    protected int[] emulateExecuteBatch()       throws java.sql.BatchUpdateException;
    protected void cleanUpBatch();
    protected void setPrecisionScaleArgs(java.lang.Object[], int[]);
    protected void setSqlType(int, int);
    protected int getSqlType(int);
    public void setCharacterStream(int, java.io.Reader, int)       throws java.sql.SQLException;
    public void setRef(int, java.sql.Ref)       throws java.sql.SQLException;
    public void setBlob(int, java.sql.Blob)       throws java.sql.SQLException;
    public void setClob(int, java.sql.Clob)       throws java.sql.SQLException;
    public void setArray(int, java.sql.Array)       throws java.sql.SQLException;
    public java.sql.ResultSetMetaData getMetaData()       throws java.sql.SQLException;
    public void setDate(int, java.sql.Date, java.util.Calendar)       throws java.sql.SQLException;
    public void setTime(int, java.sql.Time, java.util.Calendar)       throws java.sql.SQLException;
    public void setTimestamp(int, java.sql.Timestamp, java.util.Calendar)       throws java.sql.SQLException;
    public void setNull(int, int, java.lang.String)       throws java.sql.SQLException;
    public void initBoundParam()       throws java.sql.SQLException;
    protected byte[] allocBindBuf(int, int);
    protected byte[] getDataBuf(int);
    protected byte[] getLengthBuf(int);
    public int getParamLength(int);
    protected void putParamData(int)       throws java.sql.SQLException, sun.jdbc.odbc.JdbcOdbcSQLWarning;
    public void setStream(int, java.io.InputStream, int, int, int)       throws java.sql.SQLException;
    protected void setChar(int, int, int, java.lang.String)       throws java.sql.SQLException;
    protected void setBinary(int, int, byte[])       throws java.sql.SQLException;
    protected int getTypeFromObjectArray(java.lang.Object[]);
    public synchronized void close()       throws java.sql.SQLException;
    public synchronized void FreeIntParams();
    public synchronized void FreeParams()       throws java.lang.NullPointerException;
    public void setSql(java.lang.String);
    public java.lang.Object[] getObjects();
    public int[] getObjectTypes();
    public int getParamCount();
    protected void setInputParameter(int, boolean);
    public void setURL(int, java.net.URL)       throws java.sql.SQLException;
    public java.sql.ParameterMetaData getParameterMetaData()       throws java.sql.SQLException;
}
 
*/

::::::::::::::
Traverser.java
::::::::::::::
import java.util.*;

/**
$Id: traversal.html,v 1.1 2009/06/22 16:12:31 kishi Exp kishi $
@author KISHI Yasuhiro
*/

public class Traverser extends AbstractDataBaseAccessor {

    /** 親子関係を格納するMAP */
    private Map parentageMap;

    /** コンテンツのタイトルを格納するMAP */
    private Map titleMap;

    /** ルートノードID */
    private Object rootNodeId;

    public Traverser() {
        super();

        parentageMap = new TreeMap();
        titleMap = new TreeMap();
    }

    private void getRelationship() {
        try {
            this.setSQL( "select * from node order by nodeId" );

            List list = this.executeQuery();
            Iterator iterator = list.iterator();

            while ( iterator.hasNext() ) {
                Object[] record = ( Object[] ) iterator.next();

                Object nodeId = record[ 0 ];
                Object isCategory = record[ 1 ];
                Object parentNodeId = record[ 2 ];
                Object title = record[ 3 ];

                //------------------------------
                // タイトルを取得
                //------------------------------
                titleMap.put( nodeId, title );

                if ( parentNodeId == null ) {
                    //==========================================================
                    // 親ノードがNULLのものはルートである
                    //==========================================================
                    rootNodeId = nodeId;
                    continue;
                }

                //------------------------------
                // 親子関係の取得
                //------------------------------
                List children;
                if ( parentageMap.containsKey( parentNodeId ) ) {
                    children = ( List ) parentageMap.get( parentNodeId );
                } else {
                    children = new LinkedList();
                    parentageMap.put( parentNodeId, children );
                }
                children.add( nodeId );
            }

            // CLEANUP
            this.cleanup();

        } catch ( Exception e ) {
            e.printStackTrace();
        }
    }

    public void dump() {
        System.out.println( "ルートノードIDは、" + rootNodeId + " です。" );
        System.out.println( "子ノードを持っているものは以下の通りです。" );

        Iterator iterator = parentageMap.keySet().iterator();
        while ( iterator.hasNext() ) {
            Object nodeId = iterator.next();
            System.out.println( nodeId );

            List children = ( List ) parentageMap.get( nodeId );
            for ( int i = 0;i < children.size();i++ ) {
                System.out.print( "\t" + children.get( i ) );
            }
            System.out.println();
        }

        System.out.println();

    }

    /** 再帰処理のエントリポイント */
    public void traverse() {
        // ルートノードIDを表示する
        System.out.println( "(" + rootNodeId + ")" + titleMap.get( rootNodeId ) );

        // 先祖の状態を設定するフラグを配列に格納
        // とりあえず1024もあれば十分だろう（＾＾）
        boolean[] ancestor = new boolean[ 1024 ];

        /* 一応全要素をfalseに初期化 */
        for ( int i = 0; i < ancestor.length; i++ ) {
            ancestor[ i ] = false;
        }

        doTraverse( rootNodeId, 0, ancestor );
    }

    /** 再帰探索する */
    public void doTraverse( Object currentNodeId, int layer, boolean[] ancestor ) {

        List children = ( List ) parentageMap.get( currentNodeId );
        Iterator iterator = children.iterator();

        // 子ノードを出力する
        for ( int i = 0;i < children.size();i++ ) {
            Object nodeId = iterator.next();

            //=======================================================
            // 階層に応じてインデントする
            //=======================================================
            for ( int j = 0; j < layer; j++ ) {
                if ( ancestor[ j ] ) {
                    System.out.print( "│" );
                } else {
                    System.out.print( "　" );
                }
            }

            if ( i != children.size() - 1 ) {
                /* まだ弟がいる */
                System.out.print( "├" );

                /* このレイヤーに対してフラグをセット */
                /* まだ弟がいる */
                ancestor[ layer ] = true;
            } else {
                /* もう弟がいない */
                System.out.print( "└" );

                /* このレイヤーに対してフラグをセット */
                /* もう弟がいない */
                ancestor[ layer ] = false;
            }

            //=======================================================
            // 対象ノードを表示
            //=======================================================
            System.out.print( "(" + nodeId + ")" + titleMap.get( nodeId ) );
            System.out.println();

            // 子ノードがあるかチェック
            List list = ( List ) parentageMap.get( nodeId );
            if ( list != null && list.size() > 0 ) {
                // 子ノードが存在する場合はさらに再帰処理を継続
                doTraverse( nodeId, layer + 1, ancestor );
            }
        }

    }

    public static void main( String[] args ) {
        Traverser traverser = new Traverser();
        traverser.getRelationship();

        traverser.dump();

        traverser.traverse();
    }
}
```

# Bookmarkletなど
こちらも拙作となりますが、ご紹介までw:
- https://sites.google.com/site/javascripthtml5/javascript/bookmarklet/traversing-dom-by-bookmarklet