Skip to content

Instantly share code, notes, and snippets.

@Aran-Fey
Last active September 28, 2019 07:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Aran-Fey/e519280e34de5d9993f362c9021560f4 to your computer and use it in GitHub Desktop.
Save Aran-Fey/e519280e34de5d9993f362c9021560f4 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name StackOverflow auto edit
// @description Automatically detects low quality questions and attempts to improve them
// @version 1.1.11
// @author Paul Pinterits
// @include *://*.stackexchange.com/questions/*
// @include *://meta.serverfault.com/questions/*
// @include *://meta.stackoverflow.com/questions/*
// @include *://meta.superuser.com/questions/*
// @include *://serverfault.com/questions/*
// @include *://stackoverflow.com/questions/*
// @include *://superuser.com/questions/*
// @exclude *://*/questions/tagged/*
// @exclude *://*/questions/originals/*
// @namespace Aran-Fey
// @require https://github.com/Aran-Fey/userscript-lib/raw/60f9b285091e93d3879c7e94233192b7ab370821/userscript_lib.js
// @require https://github.com/Aran-Fey/SE-userscript-lib/raw/4369a5f1208fc0dddc37e43435913e1d9c2cb365/SE_userscript_lib.js
// @grant none
// @updateURL https://gist.github.com/Aran-Fey/e519280e34de5d9993f362c9021560f4/raw/SO_auto_edit.user.js
// @downloadURL https://gist.github.com/Aran-Fey/e519280e34de5d9993f362c9021560f4/raw/SO_auto_edit.user.js
// ==/UserScript==
// ==UserScript==
// @name StackOverflow auto edit
// @description Automatically detects low quality questions and attempts to improve them
// @version 1.1.11
// @author Paul Pinterits
// @include *://*.stackexchange.com/questions/*
// @include *://meta.serverfault.com/questions/*
// @include *://meta.stackoverflow.com/questions/*
// @include *://meta.superuser.com/questions/*
// @include *://serverfault.com/questions/*
// @include *://stackoverflow.com/questions/*
// @include *://superuser.com/questions/*
// @exclude *://*/questions/tagged/*
// @exclude *://*/questions/originals/*
// @namespace Aran-Fey
// @require https://github.com/Aran-Fey/userscript-lib/raw/60f9b285091e93d3879c7e94233192b7ab370821/userscript_lib.js
// @require https://github.com/Aran-Fey/SE-userscript-lib/raw/4369a5f1208fc0dddc37e43435913e1d9c2cb365/SE_userscript_lib.js
// @grant none
// @updateURL https://gist.github.com/Aran-Fey/e519280e34de5d9993f362c9021560f4/raw/SO_auto_edit.user.js
// @downloadURL https://gist.github.com/Aran-Fey/e519280e34de5d9993f362c9021560f4/raw/SO_auto_edit.user.js
// ==/UserScript==
var IMPROVEMENT_THRESHOLD = 1;
RegExp.escape = function(s) {
return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
};
function count(text, letter){
const pattern = RegExp.escape(letter);
const regex = new RegExp(pattern, 'g');
return (text.match(regex) || []).length;
}
Array.prototype.equals = function(a1, a2){
if (typeof a1 !== typeof a2)
return false;
return a1.length === a2.length && a1.every((v,i)=> v === a2[i]);
}
function add_or_exchange_tag(tags, new_tag, old_tag){
if (tags.length < 5)
tags.push(new_tag)
else {
if (old_tag === undefined)
return;
const index = tags.indexOf(old_tag);
tags[index] = new_tag;
}
}
function improve_tags(tags, post){
const result = {tags: tags.slice(),
tag_improvement: 0};
function put_tag(tag, old_tag){
add_or_exchange_tag(result.tags, tag, old_tag);
// FIXME: if a tag was replaced or the tag couldn't be added at all,
// reduce the improvement value
result.tag_improvement += 2;
}
if (!result.tags.includes('python')){
// first, check if theres' a version-specific tag that we can swap out
// in case the question has already 5 tags. If there isn't, try adding
// a "python" tag without removing any other tags.
const tag = result.tags.find(tag => /^python-[23](?:\.[x\d])?$/.test(tag));
if (tag !== undefined)
put_tag('python', tag);
else if (result.tags.find(tag => /^python-/.test(tag)))
put_tag('python');
}
// check the post's content to detect any tags that might be relevant
if (post){
const text = post.extract_text();
const TAGS = new Map([
['pandas', /\bdataframe\b/i],
['tkinter', /\btkinter\b/i],
]);
for (const pair of TAGS){
const tag = pair[0];
const regex = pair[1];
if (post.tags.includes(tag))
continue;
if (!text.match(regex))
continue;
put_tag(tag);
}
}
return result;
}
Post.prototype.improve = function(){
const root = PostBodyRoot.from_element(this.element);
const result = root.improve();
result.element = result.elements[0];
delete result.elements;
return result;
}
Question.prototype.improve = function(){
var improvement = 0;
var summary = [];
// improve the title
const res = new PostBodyText(this.title).improve(true);
var title = new PostBodyRoot(res.elements).extract_text();
improvement += res.improvement;
summary = summary.concat(res.summary);
// capitalize the first letter
title = title[0].toUpperCase() + title.substring(1);
// collapse whitespace
title = title.replace(/\s+/g, ' ');
// remove "[PYTHON]" or "PYTHON:" or similar junk.
// in order to catch things like "python3", we'll allow version numbers
// after each tag
const tags = '(?:'+ this.tags.map(RegExp.escape).join('|') + ')(?: *(?:-\\s*)?\\d+(?:\\.\\d+)*)?';
const regexes = [
'\\s*\\['+tags+'\\]\\s*',
'\\s*\\('+tags+'\\)\\s*',
'^'+tags+'\\s*[-:]\\s*',
]
var changed = false;
for (var regex of regexes){
regex = new RegExp(regex, 'i');
if (regex.test(title)){
title = title.replace(regex, '');
changed = true;
improvement += 2;
}
}
if (changed)
summary.push('Removed noise from question title');
// improve the tags
const tag_result = improve_tags(this.tags, this);
// improve the content
const result = Post.prototype.improve.apply(this);
result.title = title;
result.tags = tag_result.tags;
result.tag_improvement = tag_result.tag_improvement;
result.improvement += improvement;
result.summary = summary.concat(result.summary);
return result;
}
PostBodyElement.prototype.improve = function(){
return {elements: [this],
improvement: 0,
summary: []};
};
PostBodyContainer.prototype.improve = function(){
var elements = [];
var summary = [];
var improvement = 0;
var prev_child = null;
for (var child of this.children){
var result = child.improve();
// merge Text elements that turned into a CodeBlock with any surrounding CodeBlocks
if (result.elements.length > 0 && elements.length > 0){
if (child.is_a(PostBodyText)){
if (result.elements[0].is_a(PostBodyCodeBlock)
&& elements[elements.length-1].is_a(PostBodyCodeBlock)){
elements[elements.length-1].code += '\n\n' + result.elements[0].code;
result.elements.splice(0, 1); // this deletes the first element
}
} else if (child.is_a(PostBodyCodeBlock)){
if (result.elements[0].is_a(PostBodyCodeBlock)
&& prev_child !== null
&& prev_child.is_a(PostBodyText)
&& elements[elements.length-1].is_a(PostBodyCodeBlock)){
elements[elements.length-1].code += '\n\n' + result.elements[0].code;
result.elements.splice(0, 1); // this deletes the first element
}
}
}
elements = elements.concat(result.elements);
summary = summary.concat(result.summary);
improvement += result.improvement;
prev_child = child;
}
const container = this.clone_without_children();
container.children = elements;
return {elements: [container],
improvement: improvement,
summary: summary};
};
PostBodyDummyElement.prototype.improve = function(){
return {elements: [],
improvement: 0,
summary: []};
};
PostBodyCodeBlock.prototype.improve = function(){
var code = this.code;
var improvement = 0;
var summary = [];
if (code.trim().length == 0)
return {elements: [this],
improvement: 0,
summary: []};
// dedent the code
var indent = 9999;
for (const line of code.split('\n')){
for (var i = 0; i < line.length; i++){
if (line[i] !== ' '){
if (i < indent)
indent = i;
break;
}
}
if (indent === 0)
break;
}
if (indent > 0){
code = code.replace(new RegExp('^'+' '.repeat(indent), 'gm'), '');
improvement += 2;
summary.push('Dedented code block');
}
// remove stray backticks from failed formatting
if (code.charAt(0) === '`')
code = code.substr(1).trimLeft();
if (code.length > 0 && code.charAt(code.length-1) === '`')
code = code.substr(0, code.length-1).trimRight();
// sometimes people surround code blocks with asterisks or other formatting
var start = code.charAt(0);
var end = code.charAt(code.length-1);
if (start == end && '*'.includes(start)){
// sometimes code blocks contain triangles or squares or other things drawn
// with asterisks, so we don't want to strip the asterisks if that's the case.
// We'll assume that a large number of asterisks means it's some kind of ascii art
if (start != '*' || count(code, '*') < count(code, '\n')){
code = code.substring(1, code.length-1);
improvement += 2;
summary.push('Removed accidental formatting in code block');
}
}
// if every other line is empty, remove all empty lines
var lines = code.split(/\n\s*\n/);
if (lines.length > 3 && (lines.length-1) * 2 == count(code, '\n')){
code = lines.join('\n');
improvement += lines.length;
summary.push('Removed empty lines in code block');
}
// format tracebacks
var improved = format_tracebacks(code);
// if there's only a single element in the result, then reduce the improvement.
// Changing a CodeBlock to a BlockQuote isn't really noteworthy.
if (improved.elements.length > 1){
improvement += improved.improvement;
summary = summary.concat(improved.summary);
}
// format_tracebacks returns PostBodyText elements, which we have to turn
// back into code blocks. While we're doing that, we'll also merge consecutive
// code blocks into one.
var elements = [];
var code = "";
for (const elem of improved.elements){
if (elem.is_a(PostBodyCodeBlock))
code += elem.code;
else if (elem.is_a(PostBodyText))
code += elem.text;
else {
if (code.length > 0){
elements.push(new PostBodyCodeBlock(code));
code = "";
}
elements.push(elem);
}
}
if (code.length > 0)
elements.push(new PostBodyCodeBlock(code));
return {elements: elements,
improvement: improvement,
summary: summary};
};
PostBodyInlineCode.prototype.improve = function(){
var elements = [];
var summary = [];
var improvement = 0;
// turn multi-line code into a code block
if (this.code.includes('\n')){
var codeblock = new PostBodyCodeBlock(this.code);
improvement += this.code.split('\n').length;
summary.push('Turned multi-line code into a code block');
// improve the code block
var improved = codeblock.improve();
elements = elements.concat(improved.elements);
improvement += improved.improvement;
summary = summary.concat(improved.summary);
} else if (this.code.trim())
elements.push(this);
return {elements: elements,
improvement: improvement,
summary: summary};
};
PostBodyJSSnippet.prototype.improve = function(){
var elements = [];
var summary = [];
var improvement = 0;
// turn it into a normal code block
const codeblock = new PostBodyCodeBlock(this.code);
improvement += 8;
summary.push('Converted snippet to code block');
const result = codeblock.improve();
elements = elements.concat(result.elements);
improvement += result.improvement;
summary = summary.concat(result.summary);
return {elements: elements,
improvement: improvement,
summary: summary};
};
PostBodyText.prototype.improve = function(plain_text_only){
var elements = [this];
var summary = [];
var improvement = 0;
function wrap_code(text){
var regex = /(?:^|(?:\n\s*?)+)(?:\s*(?:\b(?:(?:import\s|from\s.*\simport\s|return\b|yield\b|del\s|[\w.]+\s*=).*|(?:(?:def|fun|function)[^(]*\([^)]*\)|(?:if|elif|else|for|while|with|try|except)\b.*\n)[:{]?)|(?:#|\/\/).*|[\w.]+\s*\([^)]*\)+))+/g;
function is_valid_match(match){
return true;
}
function get_offsets(match){
var code = match[0];
var m = code.match(/^(\s*)([^]*?)([\s.,]*)$/);
return [m[1].length, -1 * m[3].length];
}
function format_code(match){
var code = match[0];
var m = code.match(/^(\s*)([^]*?)([\s.,]*)$/);
code = m[2];
var code_element;
// if (code.includes('\n'))
// code_element = new PostBodyCodeBlock(code);
// else
// code_element = new PostBodyInlineCode(code);
code_element = new PostBodyCodeBlock(code);
var result = code_element.improve();
result.improvement += 2 + count(code, '\n');
result.summary.splice(0, 0, 'Turned plain text into code block');
return result;
}
return improve_text_with_regex(text, regex, format_code, is_valid_match, get_offsets);
}
function wrap_inline_code(text){
const regex = /(?:\w+(?:\1|(?=[.(])())(?:\(.*?\)+)?(?:\.(?=\w))?)+;?/g;
function format_code(match){
return {elements: [new PostBodyInlineCode(match[0])],
improvement: 1,
summary: ['Turned plain text into inline code']};
}
function predicate(match){
// make sure it's not just a random word at the end of a sentence
if (!match[0].includes('.') && !match[0].includes('('))
return false;
// make sure it's not just a number or version number
if (/\d+(?:\.\d+)$/.test(match[0]))
return false;
// sometimes people forget to put a space before parentheses, which
// makes the regex think the text is code. If we detect a bunch of
// words in parentheses, we'll cancel the match.
if (/[^(]+\([^, ]+ [^, ]+/.test(match[0]))
return false;
return true;
}
function get_offsets(match){
var m = /^(\s*)/.exec(match[0]);
return [m[1].length, 0];
}
return improve_text_with_regex(text, regex, format_code, predicate, get_offsets);
}
function convert_triple_backticks_to_code(text){
var regex = /```\s*([^]*?)\s*```/g;
function format_codeblock(match){
return {elements: [new PostBodyCodeBlock(match[1])],
improvement: 10,
summary: ['Converted triple backticks to code block']};
}
return improve_text_with_regex(text, regex, format_codeblock);
}
function remove_fluff(text){
var improvement = 0;
var summary = [];
while(true){
var regex = /\b(?:(?:Many )?Thanks|Thank you|(?:I )?Appreciate)(?: for (?:any|your) (?:help|answers?|patience)[.!]?| in advance[!.]*|,? \w+| ?[!.]*)?\s*$|(?:Can someone )?(?:Please,? help(?: me)?(?: (?:out )?(?:with|in|on) this)?|Help(?: me)?,? please|(?:I (?:really )?appreciate )?(?:Your|Any) (?:helps?|hints?|advices?)(?: (?:is|would be) (?:(?:greatly )?appreciated|helpful))?)[.!]*\s*(?::-?\(+\s*)?$|Cheers[!.]?\s*$/i;
var match = regex.exec(text);
if (!match)
break;
text = text.substring(0, match.index).trimRight();
improvement += 1;
}
if (improvement > 0)
summary.push('Removed fluff');
var elements = text.length == 0 ? [] : [new PostBodyText(text)];
return {elements: elements,
improvement: improvement,
summary: summary};
}
function remove_all_caps(text){
var regex = /\b[A-Z]+ [A-Z ,?!.]{3,}[A-Z]\b/g;
function lowercaseify(match){
var text = match[0];
text = text.toLowerCase(); // TODO: capitalize the first letter in each sentence
return {elements: [new PostBodyText(text)],
improvement: text.length / 2,
summary: ['Lowercased all-caps text']};
}
return improve_text_with_regex(text, regex, lowercaseify);
}
function fix_typos(text){
const REPLACEMENTS = [
[/\bi['´’](m|ve)\b/g, "I'$1"],
];
var improvement = 0;
for (const replacement of REPLACEMENTS){
const regex = replacement[0];
const repl = replacement[1];
const count = (text.match(regex) || []).length;
improvement += count * 1;
text = text.replace(regex, repl);
}
var summary = improvement > 0 ? ['Fixed typos'] : [];
return {elements: [new PostBodyText(text)],
improvement: improvement,
summary: summary}
}
function apply_improvement_function(func, elements){
var improved_elements = [];
for (var elem of elements){
// if it's not text, we don't touch it
if (!elem.is_a(PostBodyText)){
improved_elements.push(elem);
continue;
}
var result = func(elem.text);
improved_elements = improved_elements.concat(result.elements);
improvement += result.improvement;
summary = summary.concat(result.summary);
}
return improved_elements;
}
// depending on whether the result can be formatted, apply the appropriate
// improvement functions
var functions = [];
if (!plain_text_only){
functions = functions.concat([
convert_triple_backticks_to_code,
format_tracebacks,
/*wrap_code,
wrap_inline_code*/
]);
}
functions = functions.concat([
remove_fluff,
remove_all_caps,
fix_typos
]);
// the text functions can turn text into code blocks, so we'll call the functions in a loop
for (var func of functions){
elements = apply_improvement_function(func, elements);
}
return {elements: elements,
improvement: improvement,
summary: summary};
};
PostBodyBlockQuote.prototype.improve = function(){
// flatten nested block quotes
if (this.children.length == 1 && this.children[0].is_a(PostBodyBlockQuote)){
var result = this.children[0].improve();
var msg = 'Flattened block quote';
if (result.summary.length == 0 || result.summary[0] != msg)
result.summary.unshift(msg); // "unshift" is javascript for "insert"
return result;
}
return PostBodyContainer.prototype.improve.apply(this);
};
PostBodyHeading.prototype.improve = function(){
const result = PostBodyContainer.prototype.improve.apply(this);
const heading = result.elements[0];
const text = heading.extract_text();
if (count(text, ' ') > 5){
result.elements = heading.children;
result.improvement += 4;
result.summary.push('Turned heading into plain text');
}
return result;
};
function format_tracebacks(text){
var regex = /(?:(Traceback \(most recent call last\)):?)?(?:((?:\s+File ".*?", line \d+(?:, in .*)?\n(\s*).*)+)\s*)?(?:\n(\s*)\^\s*)?(\b(?=[\w.]*(?:Error|Exception))[\w.]+:\s[^\n]*)/g;
function format_traceback(match){
// if there's only an error message with no traceback
if (match[2] === undefined)
return {elements: [new PostBodyBlockQuote([new PostBodyText(match[5])])],
improvement: 5,
summary: ['Formatted error message']};
var lines = [];
var has_header = match[1] !== undefined;
if (has_header)
lines.push(match[1]+':');
for (var line of match[2].trim().split(/\n\s*/g)){
if (line.substr(0, 5) == 'File '){
if (has_header)
line = " " + line;
} else
line = " " + line;
lines.push(line);
}
// add the caret that indicates a syntax error, if it exists
if (match[4] !== undefined){
var indent = match[4].length - match[3].length + 4;
line = ' '.repeat(indent) + '^';
lines.push(line);
}
// add the exception type and error message
lines.push(match[5]);
const text = lines.join("\n");
const improvement = Math.floor((text.length - match[0].length) / 3);
const summary = [];
if (improvement > 0)
summary.push('Formatted traceback');
return {elements: [new PostBodyCodeBlock(text)],
improvement: improvement,
summary: summary};
}
function is_traceback(match){
// it's easy to confuse a "except FooError:" with an error message
var index = regex.lastIndex - match[0].length;
var t = text.substring(index-25, index);
return !t.includes('except ');
}
return improve_text_with_regex(text, regex, format_traceback, is_traceback);
}
function improve_text_with_regex(text, regex, transform_func, predicate, offset_func){
var elements = [];
var improvement = 0;
var summary = [];
if (predicate === undefined)
predicate = match => true;
if (offset_func === undefined)
offset_func = match => [0, 0];
var match;
var idx = 0;
while (match = regex.exec(text)){
if (!predicate(match))
continue;
var result = transform_func(match);
var end = regex.lastIndex;
var start = end - match[0].length;
var offsets = offset_func(match);
start += offsets[0];
end += offsets[1];
// if there's text before the match, add it to the result
if (start > idx){
var text_before = text.substring(idx, start);
elements.push(new PostBodyText(text_before));
}
elements = elements.concat(result.elements);
improvement += result.improvement;
summary = summary.concat(result.summary);
idx = end;
}
if (idx < text.length){
var t = text.substring(idx, text.length);
if (t.length > 0)
elements.push(new PostBodyText(t));
}
return {elements: elements,
improvement: improvement,
summary: summary};
}
var improved_posts = new Map();
function set_improvement_status(post, improvement_result){
// highlight the "edit" button
const edit_button = post.querySelector(".edit-post");
const edit_tags_button = post.querySelector('#edit-tags');
// when there's a pending edit, the button is different
// if (edit_button === null)
// edit_button = post_element.querySelector('[id^="edit-pending-"]');
if (edit_button !== null){
if (improvement_result.improvement <= 0){
// reset everything
edit_button.style.fontWeight = "normal";
edit_button.title = /.*/.exec(edit_button.title)[0];
} else {
edit_button.style.fontWeight = "bold";
edit_button.title = /.*/.exec(edit_button.title)[0];
edit_button.title += '\n\nAuto-edit summary:\n' + improvement_result.summary.join('\n');
}
}
var style;
if (improvement_result.tag_improvement <= 0)
style = 'font-weight: normal !important;';
else
style = 'font-weight: bold !important;';
edit_tags_button_style.innerHTML = '#edit-tags {'+style+'}';
}
function put_tags(taglist_element, tags){
const tag_template = taglist_element.querySelector('.s-tag');
// remove all tags
while (taglist_element.lastChild)
taglist_element.removeChild(taglist_element.lastChild);
// add the updated tags
for (const tag of tags){
const tag_elem = tag_template.cloneNode(true);
tag_elem.firstChild.textContent = tag;
taglist_element.appendChild(tag_elem);
}
// trigger a keypress event, just in case some event handlers need to
// do stuff
const textbox = document.getElementById('tageditor-replacing-tagnames--input');
textbox.dispatchEvent(new KeyboardEvent('keypress', {'key': ' '}));
}
function wait_for_textbox_and_insert_edit(post){
// when the post is edited, automatically insert the improved markup
function insert_draft(){
const improvement_result = improved_posts.get(post);
// update the body
const textbox = post.querySelector('textarea.wmd-input');
const original_text = textbox.value;
textbox.value = improvement_result.element.to_markup();
// set the edit summary
const edit_summary_elem = post.querySelector('.edit-comment');
const edit_summary = [...new Set(improvement_result.summary)].join(', ');
edit_summary_elem.value = edit_summary;
// add a "undo automatic edit" button
function add_undo_edit_button(){
const neighbor_button = post.querySelector('.hide-preview');
const undo_button = neighbor_button.cloneNode(true);
var callback_func = restore_original;
function restore_original(){
textbox.value = original_text;
edit_summary_elem.value = '';
if (post.is_a(Question))
titlebox.value = original_title;
callback_func = insert_auto_edit;
undo_button.textContent = 'restore automatic edit';
}
function insert_auto_edit(){
textbox.value = improvement_result.element.to_markup();
edit_summary_elem.value = edit_summary;
if (post.is_a(Question))
titlebox.value = improvement_result.title;
callback_func = restore_original;
undo_button.textContent = 'restore original text';
}
undo_button.textContent = 'restore original text';
undo_button.onclick = function(){
callback_func();
// trigger a keypress event so that SO updates the live preview
textbox.dispatchEvent(new KeyboardEvent('keypress', {'key': ' '}));
};
undo_button.style.float = 'right';
neighbor_button.parentElement.appendChild(undo_button);
}
add_undo_edit_button();
// if it's a question, update the title and the tags
if (!post.is_a(Question))
return;
const titlebox = post.querySelector('#title');
const original_title = titlebox.value;
titlebox.value = improvement_result.title;
const tag_list = post.querySelector('.tag-editor span');
put_tags(tag_list, improvement_result.tags);
// trigger a keypress event so that SO updates the live preview
textbox.dispatchEvent(new KeyboardEvent('keypress', {'key': ' '}));
}
function on_dom_mutation(mutations, observer){
for (const mutation of mutations){
for (const node of mutation.addedNodes){
if (node.classList !== undefined && node.classList.contains('s-tag')){
observer.disconnect();
setTimeout(insert_draft, 50);
return;
}
}
}
}
const observer_config = {childList: true, subtree: true};
new MutationObserver(on_dom_mutation).observe(post.element, observer_config);
}
function wait_for_tags(post){
const tag_container = post.querySelector('.post-taglist');
// when the post's tags are edited, automatically insert the new tags
function insert_tags(){
const improvement_result = improved_posts.get(post);
const tag_list = tag_container.querySelector('.tag-editor').firstChild;
put_tags(tag_list, improvement_result.tags);
}
var tag_found = false;
function on_dom_mutation(mutations, observer){
for (const mutation of mutations){
for (const node of mutation.addedNodes){
// wait until the tags are added, then disconnect when a non-tag
// node is added
if (node.classList !== undefined && node.classList.contains('s-tag')){
tag_found = true;
} else if (tag_found) {
observer.disconnect();
insert_tags();
return;
}
}
}
}
const observer_config = {childList: true, subtree: true};
new MutationObserver(on_dom_mutation).observe(tag_container, observer_config);
}
function handle_click_event(event){
const target = event.target;
if (target.id === 'edit-tags'){
const post = Post.from_child_element(target);
const improvement_result = improved_posts.get(post);
if (improvement_result === undefined || improvement_result.tag_improvement <= 0)
return;
wait_for_tags(post);
} else if (target.id === 'edit-tags-submit'){
const post = page.question;
const improvement_result = improved_posts.get(post);
if (improvement_result === undefined || improvement_result.tag_improvement <= 0)
return;
improvement_result.tag_improvement = 0;
set_improvement_status(post, improvement_result);
} else if (target.classList.contains('edit-post')){
const post = Post.from_child_element(target);
const improvement_result = improved_posts.get(post);
if (improvement_result === undefined)
return;
wait_for_textbox_and_insert_edit(post);
} else if (target.tagName === 'BUTTON' && target.id && target.id.startsWith('submit-button-')){
const post = Post.from_child_element(target);
const improvement_result = improved_posts.get(post);
if (improvement_result === undefined)
return;
improved_posts.delete(post);
}
}
function handle_post(post){
const improvement_result = post.improve();
improved_posts.set(post, improvement_result);
set_improvement_status(post, improvement_result);
}
// the "edit tags" button doesn't always exist and is spawned dynamically, so
// we'll simply update a CSS style instead of working with the element directly.
var edit_tags_button_style;
if (document.getElementById('question') !== null){
edit_tags_button_style = document.createElement('style');
document.body.appendChild(edit_tags_button_style);
page.transform_question(handle_post, Rerun.AFTER_CHANGE);
document.addEventListener('click', handle_click_event, true);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment