Last active
September 28, 2019 07:50
-
-
Save Aran-Fey/e519280e34de5d9993f362c9021560f4 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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== |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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