Last active
February 11, 2022 08:49
-
-
Save p3g4asus/f7f00052a928686553257a655b657dad to your computer and use it in GitHub Desktop.
This file contains hidden or 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 ZWO from description | |
// @namespace https://github.com/p3g4asus | |
// @version 2.0 | |
// @description Show zwo xml file in whatsonzwift.com | |
// @author p3g4asus | |
// @match https://whatsonzwift.com/workouts/* | |
// @icon https://www.google.com/s2/favicons?domain=whatsonzwift.com | |
// @homepageURL https://gist.github.com/p3g4asus/f7f00052a928686553257a655b657dad | |
// @updateURL https://gist.github.com/p3g4asus/f7f00052a928686553257a655b657dad/raw/show-zwo.user.js | |
// @downloadURL https://gist.github.com/p3g4asus/f7f00052a928686553257a655b657dad/raw/show-zwo.user.js | |
// @require https://code.jquery.com/jquery-3.6.0.min.js | |
// @grant none | |
// ==/UserScript== | |
(function() { | |
'use strict'; | |
//https://stackoverflow.com/a/51464792 | |
//new XMLSerializer().serializeToString(xmlObject.documentElement); | |
function escapeXml(unsafe) { | |
return unsafe.replace(/[<>&'"]/g, function (c) { | |
switch (c) { | |
case '<': return '<'; | |
case '>': return '>'; | |
case '&': return '&'; | |
case '\'': return '''; | |
case '"': return '"'; | |
} | |
}); | |
} | |
function prettyPrintXml(xmlDoc) { | |
// return new XMLSerializer().serializeToString(xmlDoc); | |
var xsltDoc = new DOMParser().parseFromString([ | |
// describes how we want to modify the XML - indent everything | |
'<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">', | |
' <xsl:strip-space elements="*"/>', | |
' <xsl:template match="para[content-style][not(text())]">', // change to just text() to strip space in text nodes | |
' <xsl:value-of select="normalize-space(.)"/>', | |
' </xsl:template>', | |
' <xsl:template match="node()|@*">', | |
' <xsl:copy><xsl:apply-templates select="node()|@*"/></xsl:copy>', | |
' </xsl:template>', | |
' <xsl:output indent="yes"/>', | |
'</xsl:stylesheet>', | |
].join('\n'), 'application/xml'); | |
var xsltProcessor = new XSLTProcessor(); | |
xsltProcessor.importStylesheet(xsltDoc); | |
var resultDoc = xsltProcessor.transformToDocument(xmlDoc); | |
var resultXml = new XMLSerializer().serializeToString(resultDoc); | |
return resultXml; | |
} | |
function parsePace(s) { | |
let pace = 2; | |
if (s=='5k') pace = 1; | |
else if (s == 'HM') pace = 3; | |
else if (s == 'MM') pace = 4; | |
else if (s == '1mi') pace = 0; | |
return pace; | |
} | |
function parseDuration(txt) { | |
let re; | |
let objout = { | |
dur: 60, | |
repeat: 0, | |
durationType: null, | |
txt: txt | |
}; | |
let dur = 60; | |
let repeat = 0; | |
if ((txt.indexOf('min') > 0 || txt.indexOf('sec') > 0 || txt.indexOf('hr') > 0) && (re = /([0-9]+x\s+)?([0-9]+hr)?\s*([0-9]+min)?\s*([0-9]+sec)?\s+/.exec(txt))) { | |
let dd = 0; | |
objout.durationType = 'time'; | |
for (let i = 1; i<re.length; i++) { | |
let trm1 = re[i]; | |
if (!trm1) continue; | |
else if ((trm1 = trm1.trim()).endsWith('x')) { | |
objout.repeat = parseInt(re[i].substring(0, re[i].length - 1)); | |
} | |
else if (trm1.endsWith('sec')) { | |
dd += parseInt(re[i].substring(0, re[i].length - 3)); | |
} | |
else if (trm1.endsWith('min')) { | |
dd += parseInt(re[i].substring(0, re[i].length - 3)) * 60; | |
} | |
else if (trm1.endsWith('hr')) { | |
dd += parseInt(re[i].substring(0, re[i].length - 2)) * 3600; | |
} | |
} | |
if (dd) objout.dur = dd; | |
objout.txt = txt.substring(re[0].length); | |
} | |
else if (re = /(?:([0-9]+)x\s+)?([0-9]+)\s+m\s+/.exec(txt)) { | |
objout.durationType = 'distance'; | |
if (re[1]) { | |
objout.repeat = parseInt(re[1]); | |
} | |
objout.dur = parseInt(re[2]); | |
objout.txt = txt.substring(re[0].length); | |
} | |
return objout; | |
} | |
let $sect = $('article.workout').nextAll('section').first(); | |
let $divr = $('<div>').addClass('row'); | |
let $div = $('<div>').addClass('one-whole').addClass('column'); | |
let $ta = $('<textarea>').css('border','solid 1px').css('width','100%').css('height', 'auto').prop('readonly', true).attr('rows',10); | |
let $descp = $('div.overview').nextAll('p').first(); | |
let $gli = $('h4.glyph-icon'); | |
$div.prepend($ta); | |
$divr.prepend($div); | |
let $divr2 = $('<div>').addClass('row'); | |
$div = $('<div>').addClass('one-whole').addClass('column'); | |
let $down = $('<a>'); | |
$div.prepend($down); | |
$divr2.prepend($div); | |
$sect.prepend($divr2); | |
$sect.prepend($divr); | |
let doc = document.implementation.createDocument("", "", null); | |
let work = doc.createElement("workout_file"); | |
let elem = doc.createElement("author") | |
let totDur = 0; | |
let namew = null; | |
let meanPower = 0.0; | |
let sport = null; | |
let errorLines = 0; | |
elem.innerHTML = "whatsonzwift.com"; | |
work.appendChild(elem); | |
elem = doc.createElement("name") | |
elem.innerHTML = escapeXml(namew = $gli.length?$gli.text().trim():''); | |
work.appendChild(elem); | |
elem = doc.createElement("description") | |
elem.innerHTML = escapeXml($descp.text()); | |
work.appendChild(elem); | |
elem = doc.createElement("sportType") | |
elem.innerHTML = (sport = $gli.hasClass('flaticon-run')?'run':'bike'); | |
work.appendChild(elem); | |
elem = doc.createElement("tags") | |
work.appendChild(elem); | |
doc.appendChild(work); | |
let workout = doc.createElement("workout"); | |
let all = ''; | |
let durationType = null; | |
let pace = null; | |
let $tbs = $('.workoutlist').children('.textbar'); | |
let rexp = /(?:from\s+([0-9]+)\s+to\s+|@\s+||([0-9]+)%\s+Incline\s+@\s+)(?:([0-9]+)rpm,\s+)?(?:([0-9]+)%\s+(?:of\s+(5k|10k|HM|MM|1mi)\s+pace|FTP)|No Incline Walk|([0-9]+)% Incline Walk)/; | |
let idx_from = 1; | |
let idx_incline0 = 2; | |
let idx_rpm = 3; | |
let idx_to = 4; | |
let idx_pace = 5; | |
let idx_incline1 = 6; | |
$tbs.each(function(idx, el) { | |
let txt = $(el).text(); | |
let o = parseDuration(txt); | |
if (o.durationType) { | |
let OffDuration = -1; | |
let OffPower = -1; | |
let o2; | |
let re, re2; | |
elem = null; | |
txt = o.txt; | |
if (re = rexp.exec(txt)) { | |
let ln = re[0].length; | |
if (txt.length > ln && txt.charAt(ln) == ',' && (o2 = parseDuration(txt.substring(ln + 1))) && o2.durationType && (re2 = rexp.exec(o2.txt))) { | |
OffPower = parseInt(re2[idx_to]); | |
OffDuration = o2.dur; | |
} | |
if (re[idx_from]) { | |
if (idx == 0) { | |
elem = doc.createElement("Warmup"); | |
} | |
else if (idx == $tbs.length - 1) { | |
elem = doc.createElement("Cooldown"); | |
} | |
else { | |
elem = doc.createElement("Ramp"); | |
} | |
elem.setAttribute('Duration', o.dur); | |
totDur += o.dur; | |
let a1, a2; | |
elem.setAttribute('PowerLow', a1 = parseInt(re[idx_from]) / 100.0); | |
elem.setAttribute('PowerHigh', a2 = parseInt(re[idx_to]) / 100.0); | |
meanPower += (a1 + a2) / 2 * o.dur; | |
} | |
else if (OffPower >= 0 && OffDuration >= 0) { | |
elem = doc.createElement("IntervalsT"); | |
if (o.repeat) elem.setAttribute('Repeat', o.repeat); | |
let a1, a2; | |
elem.setAttribute('OnDuration', o.dur); | |
elem.setAttribute('OnPower', a1 = parseInt(re[idx_to]) / 100.0); | |
elem.setAttribute('OffDuration', OffDuration); | |
elem.setAttribute('OffPower', a2 = OffPower / 100.0); | |
totDur += o.dur + OffDuration; | |
meanPower += a1 * o.dur + a2 * OffDuration; | |
} | |
else if (re[idx_incline1]) { | |
elem = doc.createElement("FreeRide"); | |
elem.setAttribute('Duration', o.dur); | |
elem.setAttribute('Incline', parseInt(re[idx_incline1]) / 100.0); | |
totDur += o.dur; | |
} | |
else { | |
let a1; | |
elem = doc.createElement("SteadyState"); | |
if (o.repeat) elem.setAttribute('Repeat', o.repeat); | |
elem.setAttribute('Duration', o.dur); | |
elem.setAttribute('Power', a1 = re[idx_to]?parseInt(re[idx_to]) / 100.0:0.5); | |
totDur += o.dur; | |
meanPower += a1 * o.dur; | |
} | |
if (re[idx_incline0]) elem.setAttribute('Incline', parseInt(re[idx_incline0]) / 100.0); | |
if (re[idx_rpm]) elem.setAttribute('RPM', parseInt(re[idx_rpm])); | |
if (re[idx_pace]) pace = parsePace(re[idx_pace]); | |
if (pace !== null) elem.setAttribute('pace', pace); | |
} | |
else if ((re = /\s*(:?free ride|free run)(:?\s+@\s*([0-9]+)rpm)?/.exec(txt))) { | |
elem = doc.createElement("FreeRide"); | |
elem.setAttribute('Duration', o.dur); | |
if (re[1]) elem.setAttribute('RPM', parseInt(re[1])); | |
totDur += o.dur; | |
} | |
else if ((re = /\s*(?:([0-9]+(?:\.[0-9]+)?)\/([0-9]+)\s+)?(Jog At Easy Pace|Med Pace|Run|Warmup|Relaxed Cool Down|Cooldown|Jog|Walk|Fast Pace)/.exec(txt))) { | |
let a1 = 0.5; | |
elem = doc.createElement("SteadyState"); | |
if (o.repeat) elem.setAttribute('Repeat', o.repeat); | |
elem.setAttribute('Duration', o.dur); | |
if (re[1] && re[2]) | |
a1 = parseFloat(re[1]) / parseInt(re[2]); | |
else if (re[3] == "Jog" || re[3] == "Jog At Easy Pace") | |
a1 = 0.65; | |
else if (re[3] == "Med Pace" || re[3] == "Relaxed Cool Down") | |
a1 = 0.8; | |
else if (re[3] == "Fast Pace") | |
a1 = 0.95; | |
elem.setAttribute('Power', a1); | |
totDur += o.dur; | |
meanPower += a1 * o.dur; | |
} | |
else if (txt == "Walk Easy Pace") { | |
if (idx == 0) { | |
elem = doc.createElement("Warmup"); | |
} | |
else if (idx == $tbs.length - 1) { | |
elem = doc.createElement("Cooldown"); | |
} | |
else { | |
elem = doc.createElement("Ramp"); | |
} | |
elem.setAttribute('Duration', o.dur); | |
totDur += o.dur; | |
let a1 = 0.3, a2 = 0.5; | |
elem.setAttribute('PowerLow', a1); | |
elem.setAttribute('PowerHigh', a2); | |
meanPower += (a1 + a2) / 2 * o.dur; | |
} | |
if (elem) workout.appendChild(elem); | |
else errorLines++; | |
if (durationType === null) { | |
elem = doc.createElement("durationType") | |
elem.innerHTML = o.durationType; | |
work.appendChild(elem); | |
durationType = o.durationType; | |
} | |
} | |
all += txt + '\n'; | |
}); | |
work.appendChild(workout); | |
let pp = prettyPrintXml(doc); | |
$ta.text(pp); | |
let blob = new Blob([pp], {type: 'text/xml'}); | |
let fn = $gli.text().trim() + '.zwo'; | |
$down.attr('download',fn).attr('href', URL.createObjectURL(blob)).text('Download ' + fn); | |
// $ta.text(prettyPrintXml(doc) + '\n\n\n' + all); | |
let url = new URL(location.href).pathname; | |
let idxend = url.lastIndexOf('/workouts/'); | |
if (idxend >= 0) { | |
idxend += '/workouts/'.length; | |
namew = url.substring(idxend).replace('/', '_'); | |
console.log(JSON.stringify({ | |
durationType: durationType, | |
name: namew, | |
pace: pace, | |
duration: totDur, | |
power: meanPower, | |
sport: sport, | |
errors: errorLines, | |
xml: pp | |
})); | |
} | |
else | |
console.error("Cannot parse any workout here"); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment