作品內的單一角色介紹文字超過二十行,則建議轉為獨立角色條目。

MediaWiki:Gadget-page-comment.js

出自 Komica wiki
跳至導覽 跳至搜尋

注意:在您儲存之後您必須清除瀏覽器快取才可看到最新的變更。

  • Firefox / Safari:按住 Shift 時點選 重新整理,或按 Ctrl-F5Ctrl-R (Mac 則為 ⌘-R)
  • Google Chrome:Ctrl-Shift-R (Mac 則為 ⌘-Shift-R)
  • Internet Explorer:按住 Ctrl 時點選 重新整理,或按 Ctrl-F5
  • Opera:前往 選單 → 設定 (在 Mac 為 Opera → 偏好設定) 然後再到 隱私 & 安全性 → 清除瀏覽資料 → 已快取的圖片與檔案
/** 因應MediaWiki某次更新後把換行\n全改成了\r\n,regex當中尋找整句中的尾端換行部份已改成"\r?\n" */

/** 找尋字串中第n次匹配的起始位置 */
function nthIndexOf(string, pattern, n){
	var len = string.length, i = -1;
	while(n > 0 && i < len){
		n--;
		i++;
		i = string.indexOf(pattern, i);
		if(i < 0)
			break;
	}
	return i;
}
/** 連線錯誤時的訊息*/
function getAjaxErrorText(jqXHR, exception){
	if (jqXHR.status === 0) {
		return '網路未連線';
	} else if (jqXHR.status === 404) {
		return '找不到頁面 [404]';
	} else if (jqXHR.status === 500) {
		return '伺服器內部錯誤 [500]';
	} else if (exception === 'parsererror') {
		return '解析失敗';
	} else if (exception === 'timeout') {
		return '已逾時';
	} else if (exception === 'abort') {
		return '程序被中止';
	} else {
		return '' + jqXHR.responseText;
	}
}

function isMobilePage() {
	/*if(navigator.userAgent.match(/Android/i)
		|| navigator.userAgent.match(/webOS/i)
		|| navigator.userAgent.match(/iPhone/i)
		|| navigator.userAgent.match(/iPad/i)
		|| navigator.userAgent.match(/iPod/i)
		|| navigator.userAgent.match(/BlackBerry/i)
		|| navigator.userAgent.match(/Windows Phone/i)
	){
		return (typeof window.orientation !== "undefined") || (navigator.userAgent.indexOf('IEMobile') !== -1);
	}*/
	// 如果mw-mf-viewport這個id存在,表示頁面以行動版顯示(不論使用哪種裝置)
	if($('#mw-mf-viewport').length){
		return true;
	}
	else{
		return false;
	}
}


/** 在頁面底下追加留言區 */
function addCommentForm(){
	// 如果FormData函式庫不存在,取消執行
	if(!FormData){
		console.error("Unable to call FormData library");
		return;
	}
	// Namespace允許清單
	//-->  0:(主要)	——可自動生成
	//-->  4:Project	——必須手動插入
	//--> 14:Category	——可自動生成
	var allowedNamespaces = [0,4,14];
	var manualInsertionNamespaces = [4];
	var isPreview = false;

	// 如果 Namespace不在允許清單中、連結存在oldid參數(是頁面歷史) 或 頁面是重定向,取消執行
	if($.inArray(mw.config.get('wgNamespaceNumber'),allowedNamespaces) < 0 || location.search.indexOf('oldid=') >= 0 || mw.config.get("wgIsRedirect") ){ 
		console.log("Detected a page which cannot have comment form(s). Aborted.");
		return;
	}	

	// 如果頁面是預覽畫面,將旗標設為true,以做為之後停用留言框所有功能(僅做為預覽用)的依據
	// 否則,如果非條目本體,即刻取消執行
	if(mw.config.get('wgAction') == "edit" || mw.config.get('wgAction') == "submit"){
		isPreview = true;
		console.log("Detected an edit page.  All fields of comment form will be disabled.");
	}
	else if(!mw.config.get('wgIsArticle')){
		console.log("Detected a page which is not an article itself. Aborted.");
		return;
	}

	// 抓取頁面全名
	var pageTitle = mw.config.get('wgPageName');
	// 計算頁面namespace是否為單數(=討論頁),如果是則設為0,否則設為1
	var talkPageOffset = ( mw.config.get('wgNamespaceNumber') + 1 ) % 2;
	// 拼裝出該頁面討論頁的名稱,如果本身是討論頁則使用相同namespace,否則namespace會加1
	// mw.config.get('wgTitle')會生成有空白無底線的狀況,所以將空白取代成底線
	var talkPageTitle = mw.config.get('wgFormattedNamespaces')[mw.config.get('wgNamespaceNumber')+talkPageOffset]+":"+(mw.config.get('wgTitle').replace(/ /g, "_"));

	// 如果頁面名稱尾端是「/投票」,表示其為KomicaWiki的投票頁,禁止留言框生成
	if(pageTitle.search(/\/投票$/) >= 0){
		console.log("Detected a vote page. Aborted.");
		return;
	}		

	/*
	 URLVariables 
	 source from https://github.com/shiyou0130011/JS-URL-Variable

	 @licence Apache-2.0
	*/
	function URLVariables(a){if(!(this instanceof URLVariables))return new URLVariables(a);this.decode(a)}
	URLVariables.prototype.decode=function(a){switch(typeof a){case "string":0<=a.indexOf("?")?a=a.substr(a.indexOf("?")+1):0>a.indexOf("=")&&(a="");a=0<a.length?a.split("&"):{};for(var b in a){var d=a[b].split("="),c=void 0===d[0]?"":decodeURIComponent(d[0]);d=void 0===d[1]?"":decodeURIComponent(d[1]);this[c]instanceof Array?this[c].push(d):(this[c]&&(this[c]=[this[c]]),this[c]=[d])}break;case "object":for(b in a)void 0!=this[b]?this[b]instanceof Array||(this[b]=[this[b]]):this[b]=[],a[b]instanceof Array?
	[].push.apply(this[b],a[b]):this[b].push(a[b])}return this};URLVariables.prototype.toString=function(){var a=[],b;for(b in this)"function"==typeof this[b]||URLVariables.prototype[b]||a.push(b);a.sort();var d=[];for(b in a){var c=a[b];this[c]instanceof Array||(this[c]=[this[c]]);this[c].sort();this[c].forEach(function(a,b,e){d.push(encodeURIComponent(c)+"="+encodeURIComponent(a))})}return d.join("&")};

	
	
	var urlVariables = new URLVariables(location.href)
	
	if(urlVariables.action && urlVariables.action != "submit"){
		console.log("Detected the action other than \"submit\". Aborted.");
		return;
	}
	
	// 記錄留言框數量用
	var formCount = 0;

	// 用於生成留言框HTML的函式 
	function generateCommentForm(){
		// 生成留言框的表單
		// 各留言框差異僅在外框的id
		var form = $("<form class='comment-form-group' id='comment-form-group-"+formCount+"'></form>")
			.append(
				$("<div class='comment-header'></div>").append(
					"顯示 <a class='section-link' href='' title=''></a> 的最新 <span class='number-comments'></span> 則留言;各留言左側皆設有回應用單選鈕"
				)
				.append($("<button title='重新讀取留言,並將單選鈕回歸預設位置,但會保留輸入欄內容'></button>").html("重新整理"))
			)
			.append(
				// data-load-datetime用於記錄留言頁讀取的時間,可做為防止留言頁更新衝突的依據
				$("<div class='comment-recent' data-load-datetime=''></div>").append($("<ul></ul>"))
			)
			.append(
				$("<div class='comment-form'></div>")
					.append(
						$("<label class='comment-label'></label>")
						.append(
							$("<input type='radio' title='新留言(加在底部)' name='reply' value='0' checked='checked' />")
						)
						.append("留言:")
						.append($("<input name='comment-text' class='comment-text' placeholder='請在此輸入留言;回應舊留言時,可圈選對象左側的單選鈕' />"))
					)
					.append(
						$("<label></label>")
							.append(
								$("<input type='checkbox' name='checkbox-signature' />")
							)
							.append("具名")
					)
					.append($("<button title='送出前,請務必確認回應對象是否正確選擇'></button>").html("留言送出"))
			)
			.append(
				$("<div class='send-message message-invalid'></div>").html("內容必須有非空白字元")
			)
			.append(
				$("<div class='send-message sending'></div>").html("留言傳送中……")
			)
			.append(
				$("<div class='send-message send-success'></div>").html("留言已送出")
			)
			.append(
				$("<div class='send-message send-fail'></div>").html("留言送出失敗")
			);
		// 設定表單送出時的動作
		$(form).submit(function(e) {
			// 阻止表單於送出時進行預設動作(含重新整理)
			e.preventDefault();
		});
		formCount++;

		// 如果頁面是編輯預覽,將所有輸入欄和按鈕全部取消功能
		if(isPreview){
			$(form).find("input,button").prop('disabled', true);
			$(form).find(".comment-text").attr("placeholder","頁面預覽中不允許留言送出");
		}
		return form;
	}

	// 顯示最新留言
	// 輸入目標的留言顯示區、顯示數量、留言頁章節、預設圈選回應目標
	function displayRecentComments(target,numberOfComments,sourceSection,replyTarget){
		var container;
		var number;
		var section;
		// 如果沒有目標區塊,則自動尋找
		if(target == null){
			container = $(".comment-form-group .comment-recent");
			// 如果還是沒找到,就給主控台一則錯誤訊息,並停止執行
			if(container.length <= 0){
				console.error("Cannot find comment form");
				return;
			}
		}
		else{
			container = target;
		}
		// 取得顯示數量的輸入值,並存入區域變數
		// 如果數量值未設定或不合法,就採用預設值(20)
		// 如果數量值超過99,則設為99則
		if(numberOfComments == null || $.type(numberOfComments) !== 'number' || Number.isNaN(numberOfComments) || numberOfComments < 1){
			number = 20;
		}
		else if(numberOfComments > 99){
			number = 99;
		}
		else{
			number = numberOfComments;
		}
		// 判斷章節號碼變數是否合法,若否則設為null,使之以整頁為單位
		if($.type(sourceSection) === 'number' && !Number.isNaN(sourceSection) && sourceSection >= 0){
			section = sourceSection;
		}
		else if($.type(sourceSection) === 'string'){
			// 預留空間,將來可能使用
			section = null;
		}
		else{
			section = null;
		}

		// 記錄讀取舊留言的日期時間
		$(container).attr("data-load-datetime",(new Date()).toISOString());

		$.ajax({
			url: mw.config.get('wgScript'),
			data:{
				title: talkPageTitle,
				//action: "edit",
				//section: section,  // 僅讀取頁面時,section不具任何意義
			},
			success: function(newSectionPageHTML){
				// 轉換取得的資料為DOM結構
				var newSectionPage = (new DOMParser).parseFromString(newSectionPageHTML, "text/html");
				// 暫存留言的變數
				var recentComments;
				// 暫存留言框標題的變數
				var formHeader = $(container).closest(".comment-form-group").find(".comment-header");
				var sectionLink = $(formHeader).find(".section-link");

				//
				console.log("api return (get talk page) =\n"+$(newSectionPage).find("#mw-content-text").html());

				// 在標題的特定span中顯示預計載入的留言數
				$(formHeader).find(".number-comments").html(""+number);
				// 尋找所有留言
				// 如果留言多於最大顯示數,只取最後的固定數量
				if(section == 0){
					// 如果章節編號為0(表示第一個章節頭之前的部份),視為異常,不顯示任何留言
					console.warning("(gadget-page-comment) Warning: Section number is zero");
				}
				if(section != null){
					// 如果有輸入正整數章節(討論串)編號,則只取屬該章節的留言
					// eq(section-1)是為了從大量的h1~h6之間找出正確的章節頭,位置為第「MediaWiki的章節編號減1」個
					// .has(".mw-headline")用於找出真正屬於章節頭的h,因其中必包含具有.mw-headline的<span>標籤
					var sectionTag = $(newSectionPage).find(
						"#mw-content-text h1"
						+",#mw-content-text h2"
						+",#mw-content-text h3"
						+",#mw-content-text h4"
						+",#mw-content-text h5"
						+",#mw-content-text h6"
						).has(".mw-headline").eq(section-1);
					//
					console.log("section tag = "+ sectionTag.html());

					// 判斷是否為行動版網頁,電腦版和行動版使用不同的方式抓取留言
					if(isMobilePage()){
						recentComments = $(sectionTag).next("div.collapsible-block").children("ul").children("li").slice( 0-number );
					}
					else{
						recentComments = $(sectionTag).nextUntil("h1,h2,h3,h4,h5,h6").filter("ul").children("li").slice( 0-number );
					}

					// 更新留言框標題資訊
					$(sectionLink).html("#"+sectionTag.find(".mw-headline").html());
					$(sectionLink).attr("title",talkPageTitle+"#"+sectionTag.find(".mw-headline").html());
					$(sectionLink).attr("href","/"+encodeURI(talkPageTitle).replace(/\?/g,"%3F")+"#"+sectionTag.find(".mw-headline").attr("id"));
				}
				else{
					var pageContentTemp;
					// 如果沒有輸入章節(討論串)編號,則從整頁取得留言
					// 自 MediaWiki 1.30 起,有使用ParserFunction的wiki會在#mw-content-text之下加入一個.mw-parser-output包住全部內容
					if($(".mw-parser-output").length){
						pageContentTemp = $(newSectionPage).find("#mw-content-text>div.mw-parser-output");
					}
					else{
						pageContentTemp = $(newSectionPage).find("#mw-content-text");
					}

					// 判斷是否為行動版網頁,電腦版和行動版使用不同的方式抓取留言
					if($(isMobilePage()).length){
						// 手機版頁面原始碼中,大小標題以下都有.collapsible-block包覆,在頁首的h1下則被沒有.collapsible-block的#mf-section-0包覆
						recentComments = $(pageContentTemp).find("#mf-section-0>ul>li,>div.collapsible-block>ul>li").slice( 0-number );
					}
					else{
						// find中使用「>」起頭時,等同於搜尋自身的直屬子節點
						recentComments = $(pageContentTemp).find(">ul>li").slice( 0-number );
					}
					// 更新留言框標題資訊
					$(sectionLink).html(talkPageTitle);
					$(sectionLink).attr("title",talkPageTitle);
					$(sectionLink).attr("href","/"+encodeURI(talkPageTitle).replace(/\?/g,"%3F"));
				}

				// 判斷是否存在留言紀錄
				if($(recentComments).length == 0){
					// 如果沒有紀錄,則顯示「沒有留言」
					container.html("<span style=\"color: red;\">沒有留言</span>");
				}
				else{
					// 如果有紀錄,則將取得的紀錄以<ul>包覆,並放入事先準備的div中
					container.html($("<ul></ul>").append(recentComments));
					
					// 為所有ul底下的li生成單選鈕
					var recentCommentLines = $(container).find("ul>li")
						.filter(function(index,element){
							// 過濾掉所有屬於<ol>、<dl>、<table>之下任何層級的li
							if($(element).parentsUntil(".container","ol,dl,table").length > 0){
								return false;
							}
							// 過濾掉本身沒有文字內容(=不正常層級的列表)的<ul>
							//> 例一:從第一層ul-li正常接第二層ul-li,文字內容會有 1個換行字元 ,第一層的li不會被過濾掉
							//-> *
							//-> **
							//
							//> 例二:原始碼直接進第二層ul-li,轉換成html後會成為 無文字內容的第一層ul-li 包著 第二層ul-li,必須濾掉第一層以防指定回應判斷方式異常
							//-> #
							//-> **
							//
							//> 不論哪一個範例,第二層ul-li都至少會有 1個換行字元 做為內容
							if($(element).clone().children().remove().end().text().length <= 0){
								return false;
							}
							return true;
					    });
					var a;
					for(a=0;a<$(recentCommentLines).length;a++){
						// 如果有被圈選的回應目標且大於0,則預先圈選
						// 通常用於留言送出且重新生成留言清單之後
						var tempString = "";
						if(replyTarget == a+1)
							tempString = "checked='checked'";
						$(recentCommentLines).eq(a).prepend("<input type='radio' title='回應這則留言' name='reply' value='"+(a+1)+"' "+tempString+"/>");

					}
				}
				// 如果頁面是編輯預覽,將所有輸入欄和按鈕全部取消功能
				if(isPreview)
					container.find("input,button").prop('disabled', true);
			},
			error: function(jqXHR, exception) {
				var msg = getAjaxErrorText(jqXHR, exception);
				// 顯示連線失敗時的訊息
				container.html("<span style=\"color:red;\">連線至 " + talkPageTitle + " 失敗:"+msg+"</span>");
			},
		});
	}
	
	// 尋找所有特製的div
	var pcommentTags = $(".pcomment-insert");
	// 列舉所有討論頁連結的可能性,用於下方的連結尋找
	// 若頁面名稱含有單引號(')或雙引號("),且沒有轉換成URI時,必須將它們變成「\'」「\"」再放進搜尋器才不會引發錯誤
	var ifForTalkLink = "[href*=\""+talkPageTitle.replace(/'|"/g,"\\$0")+"\"], " + 
			"[href*=\""+talkPageTitle.replace(/'|"/g,"\\$0")+"\"], " + 

			"[href*=\"Talk:"+encodeURIComponent(pageTitle).replace(/%2F/g,"/").replace(/'/g,"%27")+"\"], " + 
			"[href*=\"Talk:"+encodeURIComponent(pageTitle.replace(/ /g, "_")).replace(/%2F/g,"/").replace(/'/g,"%27")+"\"], " + 

			"[href*=\"\u8a0e\u8ad6:"+pageTitle.replace(/'|"/g,"\\$0")+"\"], " + 
			"[href*=\"\u8a0e\u8ad6:"+pageTitle.replace(/'|"/g,"\\$0").replace(/ /g, "_")+"\"], " + 

			"[href*=\""+encodeURIComponent("\u8a0e\u8ad6")+":"+encodeURIComponent(pageTitle).replace(/%2F/g,"/").replace(/'/g,"%27")+"\"]," + 
			"[href*=\""+encodeURIComponent("\u8a0e\u8ad6")+":"+encodeURIComponent(pageTitle.replace(/ /g, "_")).replace(/%2F/g,"/").replace(/'/g,"%27")+"\"]"
			;

	// 生成留言框,並顯示最新留言
	if($(".noarticletext").length > 0){
		// 如果出現 class="noarticletext ..." ,表示本頁不存在,取消生成留言框
		console.log("Article of current title does not exist.  Comment form creation aborted.");
		return;
	}
	else if	( pcommentTags.length > 0){
		// 如果頁中存在特製的div,則為每個div各生成一個留言框
		var a;
		for(a=0;a<pcommentTags.length;a++){
			var currentTag = $(pcommentTags).eq(a);
			var numberIn = Number.parseInt($(currentTag).attr('data-number'),10);
			if(Number.isNaN(numberIn)){
				numberIn = null;
			}
			var sectionIn = Number.parseInt($(currentTag).attr('data-section'),10);
			if(Number.isNaN(numberIn)){
				//sectionIn = $(currentTag).attr('data-section');
				sectionIn = null;
			}

			$(currentTag).html(generateCommentForm());
			displayRecentComments($(currentTag).find('.comment-recent'),numberIn,sectionIn);
		}
		console.log("Generated "+a+" comment form(s) inside Template:pcomment tags");		
	}
	else if( $.inArray(mw.config.get('wgNamespaceNumber'),manualInsertionNamespaces) >= 0){
		// 如果Namespace屬於必須手動插入的群組,停止執行
		console.log("Namespace ID ("+mw.config.get('wgNamespaceNumber') +") requires manual insertion");
		return;
	}
	else if( $("#mw-content-text").is(":has(" + ifForTalkLink +")") ){
		// 如果頁中存在討論頁連結,則將留言框加在該連結的正下方
		$("#mw-content-text").find(ifForTalkLink)
			.last()		// 只取條目中最後一個討論頁連結,以避免留言框重複生成
			.after(generateCommentForm());
		displayRecentComments();
		console.log("Generated a comment form right below the last talk page link")
	}
	else if( mw.config.get('wgNamespaceNumber') == 14 && $(".mw-category-generated").length > 0 ){
		// 如果是Category:之下的頁面,則將留言框加在頁面(子分類)列表之上
		$(".mw-category-generated").first().before(generateCommentForm());
		displayRecentComments();
		console.log("Generated a comment form right above the list in a category page")
	}
	else if(isPreview){
		// 如果是預覽頁面且非Category:之下,則尋找是否存在預覽區塊
		// 如果頁面存在則於其底部生成預覽用留言框,否則取消
		if( $("#wikiPreview").length > 0){
			$("#wikiPreview").append(generateCommentForm());
			console.log("Generated a read-only comment form in a preview page.")
		}
		else{
			console.error("Cannot find the preview block in a preview page.  Aborted.")
			return;
		}
	}
	else if( $("#bodyContent").is(":has(#catlinks)") ){
		// 如果存在分類頁連結,則將留言框加在其上方
		$("#catlinks").before(generateCommentForm());
		displayRecentComments();
		console.log("Generated a comment form right above the category row")
	}
	else{
		// 如果都沒有找到,則將留言框加在#bocyContent的下方
		$("#bodyContent").append(generateCommentForm());
		displayRecentComments();
		console.log("Generated a comment form right at the bottom of the page")
	}
	
	// 設定所有留言框輸入欄的focus時動作
	$('.comment-form-group input[name=\'comment-text\']').focus(function(){
		//收摺所有送出時訊息
		$(this).closest(".comment-form-group").find(".send-message").slideUp("fast", function(){
			// 預留空間
		});
	});

	// 設定所有重新整理鈕按下時的動作
	$(".comment-header button").click(function(){
		// 取得該鈕對應的外框與輸入欄,並記錄下來
		var formRoot = $(this).closest(".comment-form-group");
		// var inputBox = $(formRoot).find("input[name='comment-text']");
		var pcommentTag = $(formRoot).closest(".pcomment-insert");
		// 取得目標章節,若值不合法則設為null(目標為整頁)
		var targetSection = Number.parseInt($(pcommentTag).attr("data-section"),10);
		if(Number.isNaN(targetSection) || targetSection < 0){
			targetSection = null;
		}
		// 圈選回應目標為「新留言」(不回應,直接新增)
		$(formRoot).find("input[name=\"reply\"][value=\"0\"]").prop("checked",true);
		// 檢查有沒有pcomment區塊,有則使用自訂設定,無則使用預設
		if(pcommentTag.length <= 0)
			displayRecentComments($(formRoot).find(".comment-recent"));
		else
			displayRecentComments($(formRoot).find(".comment-recent"),
				Number.parseInt($(pcommentTag).attr("data-number"),10),
				targetSection
			);
	});

	// 設定所有留言框送出鈕按下時的動作
	$(".comment-form button").click(function(){
		// 取得該鈕對應的外框與輸入欄,並記錄下來
		var formRoot = $(this).closest(".comment-form-group");
		var inputBox = $(formRoot).find("input[name='comment-text']");
		var pcommentTag = $(formRoot).closest(".pcomment-insert");
		// 取得目標章節,若值不合法則設為null(目標為整頁)
		var targetSection = Number.parseInt($(pcommentTag).attr("data-section"),10);
		if(Number.isNaN(targetSection) || targetSection < 0){
			targetSection = null;
		}

		// 檢查留言內容
		// 如果長度為0或僅含空白字元,取消送出
		//var regexNonWhitespace = /\S/g;
		if(inputBox.val().length <= 0 || inputBox.val().search(/[\S]/g) <= -1){
			$(formRoot).find(".message-invalid").stop().slideDown("slow", function(){
				$(this).delay(2500).slideUp();
			});
			return false;
		}


		$.ajax({
			url: mw.config.get('wgScriptPath')+"/api.php",
			data:{
				"action":"query",
				"prop":"revisions",
				"titles":talkPageTitle,
				"rvlimit":"1",
				"rvprop":"timestamp",
				"format":"json"
			},
			dataType: "json",
			success: function(responseJSON){
				// 取得留言頁的最終更新時間
				var lastRevisionDateISO;
				/// var obj = JSON.parse('{"continue":{"rvcontinue":"20171125041206|58118","continue":"||"},"query":{"pages":{"1501":{"pageid":1501,"ns":8,"title":"MediaWiki:Common.js","revisions":[{"timestamp":"2017-11-25T18:20:36Z"}]}}}}');
				/// lastRevisionDateISO = obj.query.pages[1501].revisions[0].timestamp;
				// 因為目前無法藉名稱取得討論頁的id,因此不使用正規的JSON存取方式,改以regex方法讀取
				var regexTimeStamp = /"timestamp"[\s]*:[\s]*"([^"]+)"/g;
				var searchResult = regexTimeStamp.exec(JSON.stringify(responseJSON));
				if(searchResult == null || searchResult[1] == null || searchResult[1].length <= 0){
					// 如果沒有發現時間資料(=頁面尚未建立),將時間變數設為null
					lastRevisionDateISO = null;
				}
				else{
					// 如果有時間資料,將時間字串存放於時間變數
					lastRevisionDateISO = searchResult[1];
				}
				// 檢查留言頁最終更新時間是否晚於頁面讀取時間
				if(lastRevisionDateISO === undefined || $(formRoot).find(".comment-recent").attr("data-load-datetime").length == 0){
					// 如果最終更新時間未被定義(讀取時進入error)或留頁存放格沒有頁面讀取時間,而無法判斷兩者間的關係時,取消送出
					// 更新傳送失敗時的訊息
					$(formRoot).find(".send-fail").html("<span style=\"color:red;\">"+"時間資訊取得失敗,無法判斷是否有編輯衝突"+"</span>");
					console.error("Required date data cannot be loaded");
					console.error("Last edit date/time: "+lastRevisionDateISO);
					console.error("Page load date/time: "+$(formRoot).find(".comment-recent").attr("data-load-datetime"));
					return;
				}
				else if(lastRevisionDateISO !== null
					&& (new Date(lastRevisionDateISO)) > (new Date($(formRoot).find(".comment-recent").attr("data-load-datetime")) )
					){
					// 如果最終更新時間被定義為非null(=存在編輯歷史),且留言頁最終更新時間晚於頁面讀取時間,表示發生了編輯衝突
					$(formRoot).find(".send-fail").html("<span style=\"color:red;\">發生編輯衝突,請重新讀取留言再試一次</span>");
					// 顯示傳送失敗時的訊息
					$(formRoot).find(".sending").slideUp();
					$(formRoot).find(".send-fail").stop().slideDown("slow", function(){
						// 顯示失敗訊息2.5秒,並重新開放所有輸入欄位
						$(this).delay(2500).slideUp();
						$(formRoot).find("input,button").prop("disabled", false);
					});
					return;
				}

				// 如果內容含有3連以上、未被<nowiki></nowiki>包夾的波浪號,則補上該標籤
				// regex前半段用於省略已被包夾的部份
				var regexSigntime = /(< *nowiki *>.*?<\/ *nowiki *>)|(~{3,})/g;
				$(inputBox).val(
					inputBox.val().replace(regexSigntime,
						function(match,p1,p2,offset,string){
							if(p1 != null){
								return p1;
							}
							else if(p2 != null){
								return "<nowiki>"+p2+"</nowiki>";
							}
						}
					)
				);


				$.ajax({
					url: mw.config.get('wgScript'),
					data:{
						title: talkPageTitle,
						action: "edit",
						section: targetSection, // 創建新的章節時使用"new",會自動多加一個空行
					},
					success: function(newSectionPageHTML){

						// 顯示「傳送中」訊息,並暫時關閉所有輸入欄位
						$(formRoot).find(".sending").slideDown();
						$(formRoot).find("input,button").prop("disabled", true);
						
						// 將討論頁的HTML碼轉換為DOM結構
						var newSectionPage = (new DOMParser).parseFromString(newSectionPageHTML, "text/html");
						
						// 尋找該頁的頁面編輯表單;如果沒有,則在主控台顯示錯誤訊息,並停止傳送
						var editForm = $(newSectionPage).find("#editform");
						if(editForm.length == 0){
							console.error("Cannot find edit form in the talk page");
							return;
						}
						
						// 取得頁面編輯表單的內容
						var f = new FormData(editForm.get(0));
						
						// 生成插入時間戳記的語法
						// 如果有勾選「具名」則加入4個波浪號,否則加入5個
						var timestampDash;
						if($(formRoot).find("input[name='checkbox-signature']").prop("checked")){
							timestampDash = "\u007E\u007E\u007E\u007E";
						}
						else{
							timestampDash = "\u007E\u007E\u007E\u007E\u007E";
						}

						// 抓取留言原始碼,將最後的空白字元清空,再加上一個換行符號
						// 不使用 .replace(/[\s]+$/g,"\n") 是因為可能造成尾端最後一個\n被重複判定
						var commentsSource = f.get("wpTextbox1").replace(/[\s]+$/g,"") + "\n";
						var replyIndex;
						var numberOfAsterisk;
						// 取得被圈選的留言前單選鈕內部值
						var replyChecked = $(formRoot).find("input[name=reply]:checked").val();
						// 判斷單選鈕的值
						if(replyChecked < 0){
							// 異常處理器,平時不應該進入
							replyIndex = commentsSource.length;
							numberOfAsterisk = 1;
							console.error("Warning: Illegal value from reply radio button");
						}
						if(replyChecked == 0){
							// 如果值等於0,則直接留言在底部,並將星號數定為1
							replyIndex = commentsSource.length;
							numberOfAsterisk = 1;
						}
						else{
							// 如果值大於0,表示勾選了其中一則舊留言,開始搜尋回應插入點
							var searchResult;
							var replyIndexOffset;
							var commentsFiltered;

							// 複製一份Wikitext原始碼,以利之後分析
							commentsFiltered = commentsSource;

							// 將所有在html註解標籤後的列表語法移至標籤前
							// 此動作用於應對例如「(行首)*<!-- (多行)-->**」的狀況
							// 後方的g是為了儲存字串匹配結束位置
							var regexHtmlCommentTag = /<!--[\s\S]*?-->/g;
							var regexInitialLi = /^[\*#:]+/g;
							// 重複執行,直到不再有列表語法前的html註解標籤
							var timeout = 8192;
							while(timeout-- > 0){
								var searchResult0;
								var searchResult1;
								var matches = [];
								// 此RegExp物件需要在每個大迴圈重置,才能夠搜尋下一批列表語法前的html註解標籤
								var tempRegexHtmlCommentTag = new RegExp(regexHtmlCommentTag);
								var tempRegexInitialLi;
								// 開始搜尋<!---->標籤
								do{
									searchResult0 = tempRegexHtmlCommentTag.exec(commentsFiltered);
									if(searchResult0 != null){
										// 如果有找到,便檢查(搜尋)前方是否鄰接著可能做為列表語法的字元
										// 此RegExp物件需要在每個小迴圈重置,才能夠確保每次都從正確位置開始
										tempRegexInitialLi = new RegExp(regexInitialLi);
										searchResult1 = tempRegexInitialLi.exec(commentsFiltered.substring(tempRegexHtmlCommentTag.lastIndex));
										if(searchResult1 != null){
											// 如果都有找到,就將開始、中間、結束的三個位址紀錄起來
											matches.push([
												searchResult0.index,
												tempRegexHtmlCommentTag.lastIndex,
												(tempRegexHtmlCommentTag.lastIndex+tempRegexInitialLi.lastIndex)
												]);
										}
									}
								}while(searchResult0 != null);	// 直到所有<!---->都找過

								if(matches.length <= 0){
									// 如果matches是空的,表示已不存在需要對調的部份,結束大迴圈
									break;
								}
								// 將每一對匹配的<!---->和可能的列表語法前後對調
								var a;
								for(a=0;a<matches.length;a++){
									commentsFiltered =
										commentsFiltered.substring(0,matches[a][0])
										+ commentsFiltered.substring(matches[a][1],matches[a][2])
										+ commentsFiltered.substring(matches[a][0],matches[a][1])
										+ commentsFiltered.substring(matches[a][2])
										;
								}
								//清空matches以進行下一次搜尋
								matches = [];
							}
							if(timeout <= 0)
								console.error("Warning: infinite loop detected");

							// 用於過濾掉無關回覆的HTML與Wikitext碼
							var regexPairTags = /(?:<!--[\s\S]*?-->)|(?:< *nowiki *>[\s\S]*?< *\/nowiki *>)|(?:(?:^|\n):*\{\|[\s\S]*?\r?\n\|\})|(?:< *includeonly *>[\s\S]*?< *\/includeonly *>)/g;
							var regexNoinclude = /< *noinclude *>([\s\S]*?)< *\/noinclude *>/g;
							// 移除<!---->、<nowiki>、{| |}、<includeonly>的語法和其中包夾的非換行文字,以及成對的<noinclude>標籤本身
							// 不將換行移除是為了當作新留言插入點的依據
							commentsFiltered = commentsFiltered.replace(
								regexPairTags,
								function(match,offset,wholeString){
									// /.*/g 不會匹配到換行字元,因此表格前的換行字元無需特別處理
				 					return match.replace(/.*/g,"");
								})
								.replace(regexNoinclude,"$1");

							// 過濾完後,檢查原始碼的第一個列表項目是否為第一層ul的li
							// 如果其非第一層ul的li,自動生成的單選鈕便無法對應到正確的位置,必須停止執行
							regexVerifyFirstUl = /^(?:(?!\*).*\n)*\*[\*#:]/g;
							searchResult = regexVerifyFirstUl.exec(commentsFiltered);
							if(searchResult != null){
								var exceptionLineNo = searchResult[0].match(/\n/g).length+1;
								// 更新傳送失敗時的訊息
								$(formRoot).find(".send-fail").html("<span style=\"color:red;\">指定回覆失敗:於 行"+exceptionLineNo+" 偵測到不合法的列表構造,需進入討論頁面使用編輯器修正(行首必須是<code>*</code>,且後方不得接續<code>*</code>、<code>:</code>或<code>#</code>)</span>");
								// 顯示傳送失敗時的訊息
								$(formRoot).find(".sending").slideUp();
								$(formRoot).find(".send-fail").stop().slideDown("slow", function(){
									// 顯示失敗訊息10秒,並重新開放所有輸入欄位
									$(this).delay(10000).slideUp();
									$(formRoot).find("input,button").prop("disabled", false);
								});
								return;
							}

							// 取得顯示中的第一層留言(第一層的<li>標籤)數量,做為之後搜尋回覆插入點的依據
							// 不使用留言數上限是為了防止已隱藏的留言被算進最新留言數 
							var numberOfLatest = $(formRoot).find(".comment-recent>ul>li").length;
							if(numberOfLatest <= 0){
								// 異常處理器,平時不應該進入
								// 如果沒有找到<li>標籤,就以原始碼最後尾為起始位置
								replyIndex = commentsSource.length;
								numberOfAsterisk = 1;
								console.error("Warning: Reply chosen, but no <li> tag was found.");
							}

							// 從過濾後留言原始碼抓取最新n筆紀錄的regex(假設顯示上限為20筆)
							//-> /(^|\n)((?:\*(?![\*#:]).*\n(?:(?=[^\*]|\*[#:\*]).*\n)*){1,20})[\s]*$/g
							//--> (^|\n) :起始位置            
							//--> (?:\*(?![\*#:]).*\n :某個第一層ul的第一行
							//--> (?:(?=[^\*]|\*[#:\*]).*\n)* :藉由抓取「該行之後的每一行,直到遇見下一個第一層ul」,抓取該ul底下的所有內容
							//---> (?=[^\*]|\*[#:\*]) :未達下一個第一層ul的可能狀況,行首可能是「非星號」或「一個星號後接續列表次階語法字元('*'、'#'或':')」
							//--> ((?: …… ){1,20}) 取得1至20筆第一層ul(越多越好),並抓取成果
							//--> [\s]*$ 對應內容最後的空白字元(含換行),以保障抓取的資料為最後的20筆

							var regexStringLatestComments = "(^|\\n)((?:\\*(?![\\*#:]).*\\r?\\n(?:(?=[^\\*]|\\*[#:\\*]).*\\r?\\n)*){"+numberOfLatest+"})[\\s]*$";
							var regexLatestComments = new RegExp(regexStringLatestComments, "g");
							// 取過濾後留言原始碼的最後n筆(以第一層ul計)
							searchResult = regexLatestComments.exec(commentsFiltered);
							if(searchResult == null){
								// 異常處理器,平時不應該進入
								// 如果沒有找到留言紀錄,就以原始碼最後尾為起始位置
								replyIndex = commentsSource.length;
								numberOfAsterisk = 1;
								console.error("Warning: Reply chosen, but not found in latest comments."+"\n"
											+"\tValue of checked comment = "+replyChecked+"\n"
											+"\tSearch Result: \""+searchResult+"\"\n"
											+"\tNumber of Latest comments: "+numberOfLatest+"\n"
											+"\tAll comments: \""+commentsFiltered+"\"");
							}
							else{
								// 如果有找到留言紀錄,就記下紀錄的起始位置,並將留言內容另外存放
								// 最新留言前一行的換行字元不含在內,因此要加「(^|\n)」的長度
								var latestCommentsIndex = searchResult.index + searchResult[1].length;
								var latestComments = searchResult[2];
								// 留言前換行數設為最新n筆留言前的數量
								// 利用 /\n/g 而不用 "\n" 是因為只用字串會無法找尋全部匹配
								var tempLinebreaks = commentsFiltered.substring(0,latestCommentsIndex).match(/\n/g);
								var numberOfLinebreaksBeforeReplied = ((tempLinebreaks == null)? 0 : tempLinebreaks.length);
								// 從最新留言中找出回覆對象的起始位置
								var regexStringRepliedInLatest = "^(?:\\*+(?![\\*#:]).*\\r?\\n(?:(?=[^\\*]|\\*+[#:]).*\\r?\\n)*){"+(replyChecked-1)+"}";
								var regexRepliedInLatest = new RegExp(regexStringRepliedInLatest, "g");
								searchResult = regexRepliedInLatest.exec(latestComments);
								if(searchResult == null){
								// 異常處理器,平時不應該進入
								// 如果沒有找到留言紀錄,就以原始碼最後尾為起始位置
									replyIndex = commentsSource.length;
									numberOfAsterisk = 1;
									console.error("Warning: Reply chosen, but unable to identify the position of replied comment.\n"
											+"\tValue of checked comment = "+replyChecked+"\n"
											+"\tSearched string: \""+latestComments+"\"\n"
											+"\tNumber of Latest comments: "+numberOfLatest+"\n"
											+"\tAll comments: \""+commentsFiltered+"\"");
								}
								else{
									// 給「留言前換行數」加上「最新留言中回覆對象前的換行數量」
									tempLinebreaks = latestComments.substring(0,regexRepliedInLatest.lastIndex).match(/\n/g);
									numberOfLinebreaksBeforeReplied += ((tempLinebreaks == null)? 0 : tempLinebreaks.length);
									// 將回覆對象與其後的留言取出,以做最後一次分析
									var latestCommentsFromReplied = latestComments.substring(regexRepliedInLatest.lastIndex);
									// 找出正確的留言插入點
									// 會在回覆對象同級以下的留言起始點或留言尾端停止
									var regexReplyIndexInLatest = /^(\*+)(?![\*#:]).*\r?\n(?:(?=[^\*]|\1[\*#:]).*\r?\n)*/g;
									searchResult = regexReplyIndexInLatest.exec(latestCommentsFromReplied);
									if(searchResult == null){
									// 異常處理器,平時不應該進入
									// 如果沒有找到留言紀錄,就以原始碼最後尾為起始位置
										replyIndex = commentsSource.length;
										numberOfAsterisk = 1;
										console.error("Warning: Reply chosen, but unable to identify the number of linebreaks before insertion point.\n"
											+"\tValue of checked comment = "+replyChecked+"\n"
											+"\tSearched string: \""+latestCommentsFromReplied+"\"\n"
											+"\tLatest comments: \""+latestComments+"\"\n"
											+"\tNumber of Latest comments: "+numberOfLatest+"\n"
											+"\tAll comments: \""+commentsFiltered+"\"");
									}
									else{
										// 給「留言前換行數」加上「回覆插入點前的換行數量」
										// 「回覆插入點」為換行字元之後
										tempLinebreaks = latestCommentsFromReplied.substring(0,regexReplyIndexInLatest.lastIndex).match(/\n/g);
										numberOfLinebreaksBeforeReplied += ((tempLinebreaks == null)? 0 : tempLinebreaks.length);
										// 星號數量(回覆的列表層級)為回覆對象的擁有數量加1
										numberOfAsterisk = searchResult[1].length+1;
										// 從原始碼(未過濾)中取得真正的回覆插入位置,在指定的換行字元之後
										replyIndex = nthIndexOf(commentsSource,"\n",numberOfLinebreaksBeforeReplied) + 1;
										if(replyIndex < 0){
											// 異常處理器,平時不應該進入
											// 如果沒有找到正確的換行字元位置,就設為留言最尾端
											replyIndex = commentsSource.length;
											numberOfAsterisk = 1;
											console.error("Warning: Reply chosen, but unable to identify the position of reply insertion.\n"
											+"\tumberOfLinebreaksBeforeReplied = "+numberOfLinebreaksBeforeReplied+"\n"
											+"\treplyIndex"+replyIndex+"\n"
											+"\tnumberOfAsterisk"+numberOfAsterisk+"\n"
											+"\tSearched string: \""+latestCommentsFromReplied+"\"");
										}
									}
								}
							}
						}

						var asterisk = "";
						var a;
						for(a=0;a<numberOfAsterisk;a++){
							asterisk += "*";
						}

						// 送出的留言訊息
						// 格式為:
						// 「* 留言訊息 ——簽名」
						// 在留言紀錄的回覆插入點位置插入新留言
						f.set("wpTextbox1",
							commentsSource.substring(0,replyIndex)
						 	+ asterisk +" "+ inputBox.val() + " ——" + timestampDash + "\n"
						 	+ commentsSource.substring(replyIndex)
						 );
						console.log("commentsSource = "+commentsSource+"\n"
							+"replyIndex = "+replyIndex+"\n");

						var action = editForm.attr("action").split("?")[0]
						action += "?" + new URLVariables({
							title: talkPageTitle,
							action: "submit"
						})
						
						var xhr = new XMLHttpRequest;
						
						xhr.addEventListener("readystatechange", function(){
							switch(this.readyState){

								case XMLHttpRequest.DONE:	// 留言送出成功

									// 將「傳送中」訊息捲起,並將「留言完成」訊息展開
									$(formRoot).find(".sending").slideUp();
									$(formRoot).find(".send-success").stop().slideDown("slow", function(){
										// 清空輸入欄,顯示成功訊息2.5秒,並重新開放所有輸入欄位
										$(inputBox).val("");
										$(this).delay(2500).slideUp();
										$(formRoot).find("input,button").prop("disabled", false);
									});

									// 重新顯示留言內容並圈選回應目標
									if(pcommentTag.length <= 0)
										displayRecentComments($(formRoot).find(".comment-recent"),null,null,replyChecked);
									else
										displayRecentComments($(formRoot).find(".comment-recent"),
											Number.parseInt($(pcommentTag).attr("data-number"),10),
											targetSection,
											replyChecked
										);
									break;
								default:
							}
						})
						
						xhr.open("post", action)
						
						xhr.send(f)
					},
					error: function(jqXHR, exception) {
						
						var msg = getAjaxErrorText(jqXHR, exception);
						// 更新傳送失敗時的訊息
						$(formRoot).find(".send-fail").html("<span style=\"color:red;\">留言傳送失敗:"+msg+"</span>");
						// 顯示傳送失敗時的訊息
						$(formRoot).find(".sending").slideUp();
						$(formRoot).find(".send-fail").stop().slideDown("slow", function(){
							// 顯示失敗訊息2.5秒,並重新開放所有輸入欄位
							$(this).delay(2500).slideUp();
							$(formRoot).find("input,button").prop("disabled", false);
						});
					},
				});
			},
			error: function(jqXHR, exception) {
				
				var msg = getAjaxErrorText(jqXHR, exception);
				// 更新傳送失敗時的訊息
				$(formRoot).find(".send-fail").html("<span style=\"color:red;\">API介面存取失敗:"+msg+"</span>");
				// 顯示傳送失敗時的訊息
				$(formRoot).find(".sending").slideUp();
				$(formRoot).find(".send-fail").stop().slideDown("slow", function(){
					// 顯示失敗訊息2.5秒,並重新開放所有輸入欄位
					$(this).delay(2500).slideUp();
					$(formRoot).find("input,button").prop("disabled", false);
				});
			},
		});

	
		return false;
	});
}
addCommentForm();