◆少前百科是非盈利性、非官方的少女前线维基百科。 ◆如果您发现某些内容错误/空缺,请勇于修正/添加!参与进来其实很容易!点这里 加入少前百科。 ◆有任何意见、建议、纠错,欢迎在 GFwiki:反馈与建议 提出和讨论。编辑事务讨论QQ群:597764980,微博@GFwiki少前百科 ◆To foreigners,You can use twitter to contact us. |
Widget:AvgPlayer/parser.js
import tagTypeDict from '/index.php?title=Widget:AvgPlayer/effects.js&action=raw&ctype=text/javascript';
const validTagNames = ['B', 'BR', 'C', 'CG', 'SPAN'];
function getTagType(closeTag, section=1) {
return { section: section, closeTag: closeTag };
}
function Tag(tag) {
var tagName = tag.toString();
var sep = tagName.indexOf(' ');
this.tagName = (sep != -1 ? tagName.slice(0, sep) : tagName).toLowerCase();
this.type = getTagType(...tagTypeDict[this.tagName]);
this.attributes = sep != -1 ? tagName.slice(sep+1) : undefined;
}
function castSize(match, p1, offset, string) {
var size = p1/41.3;
return `<span style="font-size:calc(${size}*100%);">`;
}
function reformat(ml, depth, isLastLine) {
const div = document.createElement('div');
let html = ml;
if (depth === 0) {
html = html.replace(/<color=(#?\w+)>(.*?)<\/color>/gi, '<span style="color:$1;">$2</span>')
.replace(/<size=(\d+)>(.*?)<\/size>/gi, '<span style="font-size:calc(($1/44)*1em);">$2</span>');
}
let optionTag, options;
if (isLastLine) {
optionTag = html.match(/<cg?>/gi);
}
if (optionTag) {
optionTag = optionTag[0];
[html, ...options] = html.split(/<cg?>/gi);
options = options.map(o => o.replace(/[<>]/g, ''));
}
div.innerHTML = html;
let nodes = [...div.childNodes];
for (const node of nodes) {
if (node.nodeType != 3 && !(node.nodeType == 1 && validTagNames.includes(node.tagName))) {
div.removeChild(node);
} else if (node.nodeType == 1) {
if (depth == 2) {
node.textContent = node.innerHTML;
continue;
}
const attributes = [];
for (const attr of node.attributes) {
attributes.push(attr.name);
}
for (const attr of attributes) {
if (attr != 'style') {
node.removeAttribute(attr);
}
}
const childTypes = [...node.childNodes].map(child => child.nodeType);
if (!node.childNodes.length || childTypes.every(type => type == 3)) {
continue;
} else {
node.innerHTML = reformat(node.innerHTML, depth+1);
}
}
}
if (isLastLine) {
let ret = div.innerHTML;
if (optionTag) {
ret += optionTag + options.join(optionTag);
}
return ret;
}
return div.innerHTML;
}
function parseInnerTags(str) {
var pos = 0, len = str.length, tags = [];
while (pos < len && str[pos] == '<') {
pos++;
var tag = [];
while (pos < len && str[pos] != '>') {
tag.push(str[pos]);
pos++;
}
pos++;
if (!tag.length) continue;
tag = new Tag(tag.join('').replace('<', ''));
if (!tag.type) console.warn(tag.tagName);
if (tag.type.closeTag !== false) {
var content = [];
while (pos < len && str[pos] != '<') {
content.push(str[pos]);
pos++;
}
pos++;
tag.content = content.join('');
if (pos < len && str[pos] == '/') {
pos += tag.tagName.length + 2;
}
}
if ((tag.type.closeTag && tag.content) || !tag.type.closeTag) {
tags.push(tag);
}
}
return tags;
}
export default class AvgParser {
constructor() {
/** @private {!number} Position in current line */
this.col_ = 0;
/** @private {!string} Line that is being processed */
this.currentRow_ = undefined;
}
/** @param {!string} s Text to set as the script */
setScript(script) {
/** @private {!Array<!string>} Array of lines in the script */
this.script_ = script.replace(/\r\n/g, '\n').replaceAll('\r', '\n').split('\n').filter(s => s);
/** @private {!number} Number of lines in the script */
this.rowcount_ = this.script_.length;
/** @private {!Set} Pictures that don't have corresponding file name in charPicDict */
this.picsMissingMapping_ = new Set();
}
/** @private Move current position in current line ahead by 1 */
succeed_() {
this.col_++;
}
/**
* Move current position in current line ahead by n.
* @private
* @param {!number} n Number of steps to move ahead.
*/
seek_(n) {
this.col_ += n;
}
/** @private Collect character avg picture file name */
collectPic_() {
var row = this.currentRow_;
var code = [], num = [];
while (true) {
let char = row[this.col_];
this.succeed_();
if (char != '(') code.push(char);
else break;
}
while (true) {
let char = row[this.col_];
this.succeed_();
if (char != ')') num.push(char);
else break;
}
code = code.join('');
num = num.join('');
var pic;
/** @desc charPicDict An object to look up image url by code and num */
if (code && num) pic = charPicDict[code+'('+num+')'];
else pic = undefined;
if (code && num && !pic) {
this.picsMissingMapping_.add(code + '(' + num + ')');
}
return [pic, row[this.col_], (code && num) ? code+'('+num+')' : undefined];
}
/**
* Collect tags following a character avg picture
* @private
* @param {!Array<!string>} endSigns Characters that signal the end of the section
* @return {!Array<!Tag>}
*/
collectTags_(endSigns) {
var row = this.currentRow_;
var tags = [];
while (row[this.col_] == '<') {
var tag = [];
this.succeed_();
while (true) {
var char = row[this.col_];
this.succeed_();
if (char != '>') tag.push(char);
else break;
}
if (!tag.length) {
this.succeed_();
continue;
}
tag = new Tag(tag.join('').replace('<', ''));
if (!tag.type) console.warn(tag.tagName);
if (tag.type.closeTag !== false) {
var content = [];
if (tag.tagName == '闪屏') {
const endTag = '</' + tag.tagName + '>', tagLen = endTag.length;
while (!(
row.slice(this.col_).toLowerCase().startsWith(endTag) ||
endSigns.includes(row[this.col_]))) {
content.push(row[this.col_]);
this.succeed_();
}
var innerTags = parseInnerTags(content.join(''));
tag.params = Object.create(null);
for (const inTag of innerTags) {
console.debug(inTag);
switch (inTag.tagName) {
case 'delay':
case 'duration':
case 'rate':
tag.params[inTag.tagName] = +inTag.content;
break;
case 'cg':
tag.params.cg = inTag.content.split(',');
break;
case 'pic':
let picSequence = inTag.content.split(',');
for (let pi = 0; pi < picSequence.length; pi++) {
let pics = picSequence[pi].slice(1, -1).split('&');
for (let pj = 0; pj < pics.length; pj++) {
pics[pj] = charPicDict[pj];
}
picSequence[pi] = pics;
}
tag.params.pic = picSequence;
}
}
} else {
while (row[this.col_] != '<' && !endSigns.includes(row[this.col_])) {
content.push(row[this.col_]);
this.succeed_();
}
}
if (!endSigns.includes(row[this.col_])) {
this.succeed_();
if (row[this.col_] == '/') {
this.seek_(tag.tagName.length+2);
tag.content = content.join('');
} else {
//this.seek_(tag.tagName.length+1);
this.col_--;
}
}
}
if ((tag.type.closeTag && tag.content) || !tag.type.closeTag) {
if (tag.tagName == '闪屏') delete tag.content;
tags.push(tag);
}
while (row[this.col_] == ' ') this.succeed_();
}
return tags;
}
/**
* Parse information of characters in a line into object
* @private
* @return {object|undefined}
*/
parseChars_() {
var row = this.currentRow_;
var speaker, pics = [], speakerIndex = -1, teles = [], misplacedTags = [], allDark = false;
if (row.startsWith('()||')) {
this.seek_(4);
return;
}
while (row.slice(this.col_, this.col_+2) != '||') {
var [pic, next, code] = this.collectPic_();
pic = {file: pic, code: code};
var tags = undefined;
if (next == '<') tags = this.collectTags_(['|']);
if (tags) {
var speakerTag = tags.find(tag => tag.tagName == 'speaker');
if (speakerTag) {
speaker = speakerTag.content;
speakerIndex = pics.length;
}
if (tags.some(tag => tag.tagName == '通讯框')) {
teles.push(pics.length);
}
var positionTag = tags.find(tag => tag.tagName == 'position');
if (positionTag) {
pic.position = positionTag.content.split(',').map(x => +x);
}
allDark = allDark || tags.some(tag => tag.tagName == '同时置暗');
pics.push(pic);
misplacedTags = misplacedTags.concat(tags.filter(tag => tag.type.section));
}
while (row[this.col_] == ';' || row[this.col_] == ' ') {
this.succeed_();
}
}
while (row[this.col_] == '|') this.succeed_();
return {
pics: pics,
speaker: speaker,
speakerIndex: speakerIndex,
teles: teles,
misplacedTags: misplacedTags,
allDark: allDark
};
}
/**
* Parse effect settings in a line into array of tags
* @private
* @return {!Array<!Tag>}
*/
parseEffects_() {
var row = this.currentRow_;
var tags = [];
if (row[this.col_] == ':' || row[this.col_] == ':') {
this.succeed_();
return [];
}
while (row[this.col_] != ':' && row[this.col_] != ':') {
tags = tags.concat(this.collectTags_([':', ':']));
while (row[this.col_] != '<' && row[this.col_] != ':' && row[this.col_] != ':') {
this.succeed_();
}
}
this.succeed_();
return tags;
}
/**
* Parse lines to convert tags into HTML elements, split at '+' sign, and find possible options
* @private
* @return {Array<string>}
*/
parseLines_() {
var row = this.currentRow_;
var line = Object.create(null);
line.lines = row.slice(this.col_).split('+');
for (let i = 0; i < line.lines.length; i++) {
line.lines[i] = reformat(line.lines[i], 0, i == line.lines.length - 1);
}
var lastLine = line.lines.pop();
var optionTag = lastLine.match(/<cg?>/gi);
var options;
if (optionTag) {
optionTag = optionTag[0].slice(1, -1);
[lastLine, ...options] = lastLine.split(/<cg?>/gi);
line.optionTag = optionTag;
line.options = options;
}
line.lines.push(lastLine);
return line;
}
/**
* Parse lines into array of objects
* @return {Array<object>|undefined}
*/
parse() {
if (!this.script_) {
console.warn('Script is not set or empty. Parsing will not procceed.');
return;
}
var unmarshalled = [];
for (var i = 0; i < this.rowcount_; i++) {
this.currentRow_ = this.script_[i];
this.col_ = 0;
var line = {};
line.chars = this.parseChars_();
line.effects = this.parseEffects_();
if (line.chars) {
line.effects = line.effects.concat(line.chars.misplacedTags);
delete line.chars.misplacedTags;
}
const branchTagPos = line.effects.findIndex(tag => tag.tagName == '分支');
if (branchTagPos != -1) {
line.branch = +line.effects[branchTagPos].content;
line.effects.splice(branchTagPos, 1);
}
Object.assign(line, this.parseLines_());
unmarshalled.push(line);
}
let turningPoint, branchLastLines = [];
for (let i = 0; i < unmarshalled.length; i++) {
if (unmarshalled[i].optionTag) {
turningPoint = i;
unmarshalled[i].entries = new Map();
} else if (unmarshalled[i].branch) {
const branch = unmarshalled[i].branch;
if (!unmarshalled[turningPoint].entries.has(branch)) {
unmarshalled[turningPoint].entries.set(branch, i);
if (!unmarshalled[i-1].optionTag) {
branchLastLines.push(i-1);
}
}
}
if (i > 0 && !unmarshalled[i].branch && unmarshalled[i-1].branch) {
for (const j of branchLastLines) {
unmarshalled[j].exit = i;
}
}
}
if (this.picsMissingMapping_.size) {
console.group('Pic_Missing');
this.picsMissingMapping_.forEach(v => {console.log(v)});
console.groupEnd();
}
return unmarshalled;
}
}