以下の内容はhttps://end0tknr.hateblo.jp/entry/20250117/1737068516より取得しました。


ViewSVN for javascript

先日までに viewvc が python3に未対応であることが分かりましたので、

GitHub - masamitsu-murase/subversion_ajax_view: Subversion server's extension to show Ajax view

を参考に、javascript による Subversion リポジトリブラウザを作成。

まだ、revisionやtag、branchの表示には対応していませんが

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <style type="text/css">
     h1 { margin:5px 0; font-size:1.5em; font-family:"HG丸ゴシックM-PRO"; }
     
     table { border-collapse: collapse; }
     thead th{
       position: sticky;
       top: 0;
       background-color:#b0e0e6;
       font-weight: normal;
       border     : solid 1px #999;
       padding    :2px; }
     td {
       border     : solid 1px #999;
       padding    :2px;}

     #items        {width:800px; }
     #items tbody tr td:nth-child(1){ width: 80px; text-align:center; }
     #items tbody tr td:nth-child(3){ width:190px; text-align:center; }
     #items tbody tr td:nth-child(4){ width: 80px; text-align:center; }
     #log_msg      {width:800px; }
     #log_msg tbody tr td:nth-child(1){ width: 80px; text-align:center; }
     #log_msg tbody tr td:nth-child(2){ width:190px; text-align:center; }
     #log_msg_file {width:800px; }
     #log_msg_file tbody tr td:nth-child(1){ width: 80px; text-align:center; }
     #log_msg_file tbody tr td:nth-child(2){ width: 150px; text-align:center;}
     #log_msg_file tbody tr td:nth-child(3){ width: 40px; text-align:center; }
    </style>
  </head>
<body>

  <h1>Current Dir</h1>

  <div id="dir_path"></div>
  
  <template id="dir_elm">
    <a href=""></a>
  </template>
  <template id="dir_sep">
    <span>/</span>
  </template>
  
  <h1>Contents</h1>
  
  <table id="items">
    <thead>
      <tr>
    <th>Revision</th>
    <th>Path</th>
    <th>Date</th>
    <th>Byte</th>
      </tr>
    </thead>
    <tbody>
    </tbody>
  </table>
  
  <template id="item_tr">
    <tr>
      <td></td><td><a href=""></a></td><td></td><td></td>
    </tr>
  </template>

  <h1>Histories</h1>
  
  <table id="log_msg">
    <thead>
      <tr>
    <th>Revision</th>
    <th>Date</th>
    <th>Message</th>
      </tr>
    </thead>
    <tbody>
    </tbody>
  </table>
  
  <template id="log_msg_tr">
    <tr>
      <td></td><td></td><td></td>
    </tr>
  </template>
  
  <table id="log_msg_file">
    <thead>
      <tr>
    <th>Revision</th>
    <th>Action</th>
    <th>Kind</th>
    <th>Path</th>
      </tr>
    </thead>
    <tbody>
    </tbody>
  </table>

  <template id="log_msg_file_tr">
    <tr>
      <td></td><td></td><td></td><td></td>
    </tr>
  </template>
  
  <script src="view_svn.js"></script>
</body>
</html>
'use strict';
const BASE_PATH = "/svn/test1/"; // repository name

class ViewSvn {
    constructor() {}
    
    init_window=async()=> {
        let req_param = this.parse_url();
        let dir = req_param["dir"] || "";
        let rev = req_param["rev"] || "";
        
        let repo_summary = await this.get_repo_summary(dir,rev);
        rev = repo_summary["svn-youngest-rev"];
        // show current dir
        let dirs = this.parse_dir( dir );
        this.disp_dirs(dirs, rev );
        
        let repo_histories = await this.get_repo_histories(dir,rev );
        this.disp_repo_histories( repo_histories );
        // show contents
        let items = await this.get_items(dir,rev);
        this.disp_items( items, dir );
    }

    disp_dirs=( dirs, rev )=>{
        let dirs_container = document.querySelector("#dir_path");
        let a_tmpl         = document.querySelector("#dir_elm");
        let sep_tmpl       = document.querySelector("#dir_sep");

        let a_elm = a_tmpl.content.cloneNode(true).querySelector("a");
        a_elm.textContent = "TOP";
        a_elm.setAttribute("href", "." );
        dirs_container.appendChild( a_elm );

        let i = 0;
        for (let dir of dirs ) {
            i += 1;
            let a_elm = a_tmpl.content.cloneNode(true).querySelector("a");
            a_elm.textContent = dir;
            a_elm.setAttribute("href", "?dir="+ dirs.slice(0,i).join("/") );

            let sep_tmpl       = document.querySelector("#dir_sep");
            dirs_container.appendChild(
                sep_tmpl.content.cloneNode(true).querySelector("span")
            );
            
            dirs_container.appendChild( a_elm );

        }
    }
    
    parse_dir=( dir_full_path )=>{
        if( dir_full_path==undefined ) return [];
        
        if( dir_full_path.slice(-1) =="/"){
            dir_full_path = dir_full_path.slice(0, -1);
        }
        if( dir_full_path.length==0 ) return [];
        let dirs =  dir_full_path.split("/");
        return dirs;
    }
    
    disp_items=( items, dir )=>{
        let tbody_elm  = document.querySelector("#items tbody");
        let tr_tmpl    = document.querySelector("#item_tr");

        const regex = new RegExp( "^"+dir );
        
        for (let item of items ) {
            let tr_elm = tr_tmpl.content.cloneNode(true);
            let td = tr_elm.querySelectorAll("td");
            
            let a_elm = tr_elm.querySelectorAll("a");
            let href_info = this.parse_item_href( item["D:href"] );

            if( href_info["path"].length == 0 || href_info["path"] == "/" ){
                continue;
            }
            
            a_elm[0].textContent =
                decodeURI( href_info["path"] ).replace(regex, '');
            if( a_elm[0].textContent.length==0){
                continue;
            }
    
            if( href_info["path"].slice(-1) =="/"){
                let req_url = "?dir="+href_info["path"];
                a_elm[0].setAttribute("href", req_url);
            } else {
                let req_url = BASE_PATH+href_info["path"];
                a_elm[0].setAttribute("href", req_url);
            }

            td[2].textContent = item["lp1:creationdate"].toLocaleString('ja-JP');

            td[3].textContent = item["lp1:getcontentlength"];
            td[0].textContent = item["lp1:version-name"];
            
            tbody_elm.appendChild( tr_elm );
        }
    }

    disp_repo_histories=( repo_histories )=>{
        let msg_tbody_elm  = document.querySelector("#log_msg tbody");
        let msg_tr_tmpl    = document.querySelector("#log_msg_tr");
        let file_tbody_elm = document.querySelector("#log_msg_file tbody");
        let file_tr_tmpl   = document.querySelector("#log_msg_file_tr");

        for (let history of repo_histories ) {
            let msg_tr_elm = msg_tr_tmpl.content.cloneNode(true);
            let msg_td = msg_tr_elm.querySelectorAll("td");
            
            msg_td[0].textContent = history["D:version-name"];
            msg_td[1].textContent = history["S:date"].toLocaleString('ja-JP');
            msg_td[2].textContent = history["D:comment"];
            msg_tbody_elm.appendChild(msg_tr_elm);


            for (let item of history["items"] ){
                let file_tr_elm = file_tr_tmpl.content.cloneNode(true);
                let file_td = file_tr_elm.querySelectorAll("td");
                file_td[0].textContent = history["D:version-name"];
                file_td[1].textContent = item["tag_name"];
                file_td[2].textContent = item["node-kind"];
                file_td[3].textContent = item["node_value"];
                file_tbody_elm.appendChild(file_tr_elm);
            }
        }
    }

    get_items=async(dir,rev)=>{
        let req_url = BASE_PATH +"!svn/bc/"+rev+"/"+dir;
        let req_xml = [
            '<?xml version="1.0" encoding="utf-8"?>',
            '<propfind xmlns="DAV:">',
            '<prop>',
            '<creator-displayname xmlns="DAV:"/>',
            '<creationdate xmlns="DAV:"/>',
            '<version-name xmlns="DAV:"/>',
            '<getcontentlength xmlns="DAV:"/>',
            '<resourcetype xmlns="DAV:"/>',
            '</prop>','</propfind>'].join("");
        let response = await fetch(req_url,{method : 'PROPFIND',
                                            headers: {'Depth':1},
                                            body   : req_xml});
        if (! response.ok ){
            return [];
        }

        let items = [];
        const parser = new DOMParser();
        let xml_data  = parser.parseFromString(await response.text(),"text/xml");

        for (let item_elm of xml_data.documentElement.childNodes ) {
            if( item_elm.childNodes.length == 0 ) {
                continue;
            }
            let item = {};
            for (let propstat of item_elm.childNodes ) {
                if( propstat.tagName==undefined ){
                    continue;
                }
                if( propstat.tagName=="D:href" ){
                    item[propstat.tagName] = propstat.childNodes[0].nodeValue;
                    continue;
                }
                for (let prop_elm of item_elm.getElementsByTagName("D:prop")) {
                    for (let attr_elm of prop_elm.childNodes ) {
                        if(attr_elm.childNodes[0]==undefined){
                            continue;
                        }
                        if( attr_elm.tagName=="lp1:creationdate" ){
                            item[attr_elm.tagName] =
                                new Date( attr_elm.childNodes[0].nodeValue );
                        } else {
                            item[attr_elm.tagName] =
                                attr_elm.childNodes[0].nodeValue;
                        }
                    }
                }
            }
            items.push( item );
        }
        return items;
    }
    
    get_repo_histories=async(dir,rev)=>{
        let req_url = BASE_PATH +"!svn/bc/"+rev+"/"+dir;
        let req_xml = [
            '<?xml version="1.0" encoding="utf-8"?>',
            '<S:log-report xmlns:S="svn:">',
            '<S:start-revision>',
            rev,
            '</S:start-revision>',
            '<S:end-revision>0</S:end-revision>',
            '<S:limit>20</S:limit>',
            '<S:discover-changed-paths />',
            '<S:revprop>svn:author</S:revprop>',
            '<S:revprop>svn:date</S:revprop>',
            '<S:revprop>svn:log</S:revprop>',
            '<S:path></S:path>',
            '</S:log-report>'].join("");
        let response = await fetch(req_url,{method : 'REPORT',
                                            headers: {},
                                            body   : req_xml});
        if (! response.ok ){
            return [];
        }

        let histories = [];
        const parser = new DOMParser();
        let xml_data  = parser.parseFromString(await response.text(),"text/xml");
        for (let log_item of xml_data.documentElement.childNodes ) {
            let history = {"items":[] };
            for (let item_elm of log_item.childNodes ) {
                if( item_elm.tagName==undefined ){
                    continue;
                }
                if(item_elm.tagName=="S:date" ){
                    history[item_elm.tagName ] =
                        new Date(item_elm.childNodes[0].nodeValue);
                    continue;
                }
                if(item_elm.tagName=="D:version-name" ||
                   item_elm.tagName=="D:comment" ){
                    history[item_elm.tagName ] = item_elm.childNodes[0].nodeValue;
                    continue;
                }

                let file_or_dir = {};
                file_or_dir["tag_name"]   = item_elm.tagName;
                file_or_dir["node-kind"]  = item_elm.getAttribute("node-kind");
                file_or_dir["node_value"] = item_elm.getAttribute("node-kind");
                file_or_dir["node_value"] = item_elm.childNodes[0].nodeValue;
                history["items"].push(file_or_dir);
            }
            if( history["items"].length == 0){
                continue;
            }
            histories.push(history);
        }
        
        return histories;
    }

    get_repo_summary=async(dir,rev)=>{
        if(! rev){
            rev="HEAD";
        }
        
        let req_url = BASE_PATH +"!svn/bc/"+rev+"/"+dir;
        let req_xml = ['<?xml version="1.0" encoding="utf-8"?>',
                       '<D:options xmlns:D="DAV:">',
                       '<D:activity-collection-set/>',
                       '</D:options>' ].join("");
        let response = await fetch(req_url,{method : 'OPTIONS',
                                            headers: {},
                                            body   : req_xml});
        if (! response.ok ){
            return {};
        }

        let ret_data = {};
        for (let [key, value] of await response.headers) {
            ret_data[key] = value;
        }
        return ret_data;
    }

    parse_item_href=( url )=>{
        let re_pat = [ BASE_PATH,
                       "!svn/bc/([0-9]+)",
                       "(?:branches/([^/]+))?",
                       "(?:tags/([^/]+))?",
                       "/(.*)" ].join("")
        const regex = new RegExp( re_pat,"i");
        const match = regex.exec(url);
        if (! match) {
            return {"rev"   :match[1],
                    "branch":undefined,
                    "tags"  :undefined,
                    "path"  :match[4] };
        }

        return {"rev"   :match[1],
                "branch":match[2],
                "tags"  :match[3],
                "path"  :match[4] };
    }
    parse_url=()=>{
        let ret_data = {};
        const url = new URL( location.href );
        ret_data["dir"]    = url.searchParams.get('dir');
        ret_data["branch"] = url.searchParams.get('branch');
        ret_data["tag"]    = url.searchParams.get('tag');
        ret_data["rev"]    = url.searchParams.get('rev');
        return ret_data;
    }
}

let view_svn = new ViewSvn();
view_svn.init_window();



以上の内容はhttps://end0tknr.hateblo.jp/entry/20250117/1737068516より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14