diff options
| -rw-r--r-- | Cargo.lock | 7 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | src/hooks/wolfram_alpha.rs | 171 | ||||
| -rw-r--r-- | src/util/web.rs | 29 | ||||
| -rw-r--r-- | tests/resources/wolfram_alpha_api_response_of_input_with_spaces.json | 244 | ||||
| -rw-r--r-- | tests/resources/wolfram_alpha_api_response_with_no_primaries.json | 300 |
6 files changed, 733 insertions, 19 deletions
@@ -134,6 +134,7 @@ dependencies = [ "tracing", "tracing-futures", "tracing-subscriber", + "urlparse", ] [[package]] @@ -1506,6 +1507,12 @@ dependencies = [ ] [[package]] +name = "urlparse" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "110352d4e9076c67839003c7788d8604e24dcded13e0b375af3efaa8cf468517" + +[[package]] name = "version_check" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -31,6 +31,7 @@ sedregex = { version = "0.2", git = "https://gitlab.com/audron/sedregex" } rand = "0.8.3" reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "gzip"] } +urlparse = "0.7.3" [dev-dependencies] mockito = "0.30.0" diff --git a/src/hooks/wolfram_alpha.rs b/src/hooks/wolfram_alpha.rs index e440eb8..5584935 100644 --- a/src/hooks/wolfram_alpha.rs +++ b/src/hooks/wolfram_alpha.rs @@ -1,4 +1,7 @@ -use crate::util::{formatting::truncate, web::shorten_url}; +use crate::util::{ + formatting::truncate, + web::{quote_plus, shorten_url}, +}; use anyhow::{bail, Context, Error, Result}; use futures::try_join; use irc::client::prelude::*; @@ -51,7 +54,7 @@ fn to_single_string(wa_res: WaResponse) -> String { .join(" - ") } -fn get_url(query_str: &str, api_key: Option<&str>, base_url: Option<&str>) -> String { +fn get_url(query_str: &str, api_key: Option<&str>, base_url: Option<&str>) -> Result<Url, Error> { let wa_url = "http://api.wolframalpha.com"; let api_url = format!( "{}/v2/query?input={}&appid={}&output=json", @@ -59,11 +62,11 @@ fn get_url(query_str: &str, api_key: Option<&str>, base_url: Option<&str>) -> St query_str, api_key.unwrap_or("XXX"), // Allow tests to run without a key ); - api_url + Url::parse(&api_url).context("Failed to parse URL") } -async fn send_wa_req(url: &str) -> Result<String, Error> { - let body = get(Url::parse(url).context("Failed to parse url")?) +async fn send_wa_req(url: &Url) -> Result<String, Error> { + let body = get(url.to_owned()) .await .context("Failed to make request")? .text() @@ -72,12 +75,24 @@ async fn send_wa_req(url: &str) -> Result<String, Error> { Ok(body) } -async fn handle_wa_req(url: &str) -> Result<WaResponse, Error> { +async fn handle_wa_req(url: &Url) -> Result<WaResponse, Error> { let res_body = send_wa_req(url).await?; let parsed = serde_json::from_str(&res_body)?; Ok(parsed) } +/// Gets a URL users can click, leading to the main WA page. +async fn get_wa_user_short_url(input: &str) -> Result<String, Error> { + let user_url = format!( + "http://www.wolframalpha.com/input/?i={}", + // For some reason some inputs need double quote calls, e.g. '1 * 2'. + // Maybe only with is.gd though. + quote_plus("e_plus(input)?)? + ); + + shorten_url(&user_url).await +} + /// Sends a request to the Wolfram Alpha API, returns a plain text response. #[tracing::instrument] async fn wa_query( @@ -85,34 +100,42 @@ async fn wa_query( api_key: Option<&str>, base_url: Option<&str>, ) -> Result<String, Error> { - let user_url = format!("http://www.wolframalpha.com/input/?i={}", query_str); - let user_url_shortened_fut = shorten_url(&user_url); - - let url = get_url(query_str, api_key, base_url); + let user_url_shortened_fut = get_wa_user_short_url(query_str); + let url = get_url(query_str, api_key, base_url)?; let wa_res_fut = handle_wa_req(&url); // Can't just (foo.await, bar.await), smh // https://rust-lang.github.io/async-book/06_multiple_futures/02_join.html let (wa_res, user_url_shortened) = try_join!(wa_res_fut, user_url_shortened_fut)?; + let string_result = match to_single_string(wa_res) { + x if x.is_empty() => "No primary results.".to_string(), + x => x, + }; + Ok(format!( "{} - {}", - truncate(&to_single_string(wa_res), 250), // Same length as in gonzobot + truncate(&string_result, 250), // Same length as in gonzobot &user_url_shortened, )) } +fn get_input_query(text: &str) -> Result<String, Error> { + let input = text.chars().as_str().splitn(2, " ").collect::<Vec<&str>>(); + if input.len() != 2 { + bail!("Empty input for WA query"); + } + let content = input[1].trim(); + Ok(content.to_string()) +} + pub async fn wa(bot: &crate::Bot, msg: Message) -> Result<()> { privmsg!(msg, { - let input = text.chars().as_str().splitn(2, " ").collect::<Vec<&str>>(); - if input.len() != 2 { - bail!("Empty input for WA query"); - } - let content = input[1].trim(); + let content = get_input_query(text)?; bot.send_privmsg( msg.response_target() .context("failed to get response target")?, - &wa_query(content, Some(&bot.config.settings.wa_api_key), None).await?, + &wa_query(&content, Some(&bot.config.settings.wa_api_key), None).await?, )?; }) } @@ -120,12 +143,86 @@ pub async fn wa(bot: &crate::Bot, msg: Message) -> Result<()> { #[cfg(test)] mod tests { - use super::wa_query; + use super::{get_input_query, get_wa_user_short_url, wa_query}; use anyhow::{Error, Result}; use mockito::{self, Matcher}; + #[test] + fn test_input_query_content_retrieval() -> Result<(), Error> { + let incoming = ":wa test"; + let content = get_input_query(incoming)?; + assert_eq!(content, "test"); + Ok(()) + } + + #[test] + fn test_input_query_content_retrieval_with_spaces() -> Result<(), Error> { + let incoming = ":wa foo bar"; + let content = get_input_query(incoming)?; + assert_eq!(content, "foo bar"); + Ok(()) + } + + #[test] + fn test_input_query_content_retrieval_with_more_spaces() -> Result<(), Error> { + let incoming = ":wa foo bar baz"; + let content = get_input_query(incoming)?; + assert_eq!(content, "foo bar baz"); + Ok(()) + } + + // These tests must be updated if a service other than is.gd is used #[tokio::test] - async fn test_query_result_json_parsing() -> Result<(), Error> { + async fn test_wa_user_short_url_1() -> Result<(), Error> { + let input = "5/10"; + assert_eq!(get_wa_user_short_url(input).await?, "https://is.gd/kgsSb7"); + Ok(()) + } + + #[tokio::test] + async fn test_wa_user_short_url_2() -> Result<(), Error> { + let input = "5 / 10"; + assert_eq!(get_wa_user_short_url(input).await?, "https://is.gd/b4xgwD"); + Ok(()) + } + + #[tokio::test] + async fn test_wa_user_short_url_3() -> Result<(), Error> { + let input = "1*2"; + assert_eq!(get_wa_user_short_url(input).await?, "https://is.gd/TLGKih"); + Ok(()) + } + + #[tokio::test] + async fn test_wa_user_short_url_4() -> Result<(), Error> { + let input = "1 * 2"; + assert_eq!(get_wa_user_short_url(input).await?, "https://is.gd/ZXYAUR"); + Ok(()) + } + + #[tokio::test] + async fn test_wa_user_short_url_5() -> Result<(), Error> { + let input = "3+4"; + assert_eq!(get_wa_user_short_url(input).await?, "https://is.gd/AZRBWy"); + Ok(()) + } + + #[tokio::test] + async fn test_wa_user_short_url_6() -> Result<(), Error> { + let input = "3 + 4"; + assert_eq!(get_wa_user_short_url(input).await?, "https://is.gd/lBuhgK"); + Ok(()) + } + + #[tokio::test] + async fn test_wa_user_short_url_7() -> Result<(), Error> { + let input = "test"; + assert_eq!(get_wa_user_short_url(input).await?, "https://is.gd/NzIyUZ"); + Ok(()) + } + + #[tokio::test] + async fn test_query_result_parsing() -> Result<(), Error> { let body = include_str!("../../tests/resources/wolfram_alpha_api_response.json"); let _m = mockito::mock("GET", Matcher::Any) // Trimmed down version of a full WA response: @@ -141,4 +238,40 @@ mod tests { ); Ok(()) } + + #[tokio::test] + async fn test_query_with_spaces_result_parsing() -> Result<(), Error> { + let body = include_str!( + "../../tests/resources/wolfram_alpha_api_response_of_input_with_spaces.json" + ); + let _m = mockito::mock("GET", Matcher::Any) + // Trimmed down version of a full WA response: + .with_body(body) + .create(); + mockito::start(); + + let res = wa_query("5 / 10", None, Some(&mockito::server_url())).await?; + let res_without_link = res.rsplitn(2, "-").collect::<Vec<&str>>()[1..].join(" "); + assert_eq!( + res_without_link.trim(), + "Exact result: 1/2 - Decimal form: 0.5" + ); + Ok(()) + } + + #[tokio::test] + async fn test_query_with_result_with_primary_pods_parsing() -> Result<(), Error> { + let body = + include_str!("../../tests/resources/wolfram_alpha_api_response_with_no_primaries.json"); + let _m = mockito::mock("GET", Matcher::Any) + // Trimmed down version of a full WA response: + .with_body(body) + .create(); + mockito::start(); + + let res = wa_query("what is a url", None, Some(&mockito::server_url())).await?; + let res_without_link = res.rsplitn(2, "-").collect::<Vec<&str>>()[1..].join(" "); + assert_eq!(res_without_link.trim(), "No primary results."); + Ok(()) + } } diff --git a/src/util/web.rs b/src/util/web.rs index 77e20a3..bfa7003 100644 --- a/src/util/web.rs +++ b/src/util/web.rs @@ -1,5 +1,10 @@ use anyhow::{Context, Error, Result}; use reqwest::{get, Url}; +use urlparse::quote_plus as urlparse_quote_plus; + +pub(crate) fn quote_plus(text: &str) -> Result<String, Error> { + Ok(urlparse_quote_plus(text, b"")?) +} // TODO: Either catinator should have a URL shortening utility module, // or we should start our own service @@ -18,3 +23,27 @@ pub(crate) async fn shorten_url(url: &str) -> Result<String, Error> { Ok(short_url) } + +#[cfg(test)] +mod tests { + use super::{quote_plus}; + use anyhow::{Error, Result}; + + #[test] + fn test_quote_plus_1() -> Result<(), Error> { + assert_eq!(quote_plus("5/10")?, "5%2F10"); + Ok(()) + } + + #[test] + fn test_quote_plus_2() -> Result<(), Error> { + assert_eq!(quote_plus("1 * 2")?, "1+%2A+2"); + Ok(()) + } + + #[test] + fn test_quote_plus_3() -> Result<(), Error> { + assert_eq!(quote_plus("e_plus("1 * 2")?)?, "1%2B%252A%2B2"); + Ok(()) + } +} diff --git a/tests/resources/wolfram_alpha_api_response_of_input_with_spaces.json b/tests/resources/wolfram_alpha_api_response_of_input_with_spaces.json new file mode 100644 index 0000000..014bb80 --- /dev/null +++ b/tests/resources/wolfram_alpha_api_response_of_input_with_spaces.json @@ -0,0 +1,244 @@ +{ + "queryresult": { + "success": true, + "error": false, + "numpods": 7, + "datatypes": "Math", + "timedout": "", + "timedoutpods": "", + "timing": 0.921, + "parsetiming": 0.17500000000000002, + "parsetimedout": false, + "recalculate": "", + "id": "MSP485014edhg0e7d9e6fi500002a5e4i117igf338i", + "host": "https://www4c.wolframalpha.com", + "server": "5", + "related": "https://www4c.wolframalpha.com/api/v1/relatedQueries.jsp?id=MSPa485114edhg0e7d9e6fi500005h567id42e4fib623794414399634972455", + "version": "2.6", + "inputstring": "5 / 10", + "pods": [ + { + "title": "Input", + "scanner": "Identity", + "id": "Input", + "position": 100, + "error": false, + "numsubpods": 1, + "subpods": [ + { + "title": "", + "img": { + "src": "https://www4c.wolframalpha.com/Calculate/MSP/MSP485214edhg0e7d9e6fi500003b0hcbg1cbai5gi4?MSPStoreType=image/gif&s=5", + "alt": "5/10", + "title": "5/10", + "width": 21, + "height": 37, + "type": "Default", + "themes": "1,2,3,4,5,6,7,8,9,10,11,12", + "colorinvertable": true + }, + "plaintext": "5/10" + } + ], + "expressiontypes": { + "name": "Default" + } + }, + { + "title": "Exact result", + "scanner": "Rational", + "id": "Result", + "position": 200, + "error": false, + "numsubpods": 1, + "primary": true, + "subpods": [ + { + "title": "", + "img": { + "src": "https://www4c.wolframalpha.com/Calculate/MSP/MSP485314edhg0e7d9e6fi5000041ee77342b27hf65?MSPStoreType=image/gif&s=5", + "alt": "1/2", + "title": "1/2", + "width": 13, + "height": 37, + "type": "Default", + "themes": "1,2,3,4,5,6,7,8,9,10,11,12", + "colorinvertable": true + }, + "plaintext": "1/2" + } + ], + "expressiontypes": { + "name": "Default" + }, + "states": [ + { + "name": "Step-by-step solution", + "input": "Result__Step-by-step solution", + "stepbystep": true + } + ] + }, + { + "title": "Decimal form", + "scanner": "Numeric", + "id": "DecimalApproximation", + "position": 300, + "error": false, + "numsubpods": 1, + "primary": true, + "subpods": [ + { + "title": "", + "img": { + "src": "https://www4c.wolframalpha.com/Calculate/MSP/MSP485414edhg0e7d9e6fi50000379ibc1hdfcc9c3f?MSPStoreType=image/gif&s=5", + "alt": "0.5", + "title": "0.5", + "width": 30, + "height": 21, + "type": "Default", + "themes": "1,2,3,4,5,6,7,8,9,10,11,12", + "colorinvertable": true + }, + "plaintext": "0.5" + } + ], + "expressiontypes": { + "name": "Default" + } + }, + { + "title": "Number line", + "scanner": "NumberLine", + "id": "NumberLine", + "position": 400, + "error": false, + "numsubpods": 1, + "subpods": [ + { + "title": "", + "img": { + "src": "https://www4c.wolframalpha.com/Calculate/MSP/MSP485514edhg0e7d9e6fi500001efh7fidi29hi9d0?MSPStoreType=image/gif&s=5", + "alt": "Number line", + "title": "", + "width": 330, + "height": 59, + "type": "1DMathPlot_1", + "themes": "1,2,3,4,5,6,7,8,9,10,11,12", + "colorinvertable": true + }, + "plaintext": "" + } + ], + "expressiontypes": { + "name": "Default" + } + }, + { + "title": "Percentage", + "scanner": "Numeric", + "id": "Percentage", + "position": 500, + "error": false, + "numsubpods": 1, + "subpods": [ + { + "title": "", + "img": { + "src": "https://www4c.wolframalpha.com/Calculate/MSP/MSP485614edhg0e7d9e6fi500002de8fch4636c7c7c?MSPStoreType=image/gif&s=5", + "alt": "50%", + "title": "50%", + "width": 28, + "height": 19, + "type": "Default", + "themes": "1,2,3,4,5,6,7,8,9,10,11,12", + "colorinvertable": true + }, + "plaintext": "50%" + } + ], + "expressiontypes": { + "name": "Default" + } + }, + { + "title": "Pie chart", + "scanner": "Rational", + "id": "PieChart", + "position": 600, + "error": false, + "numsubpods": 1, + "subpods": [ + { + "title": "", + "img": { + "src": "https://www4c.wolframalpha.com/Calculate/MSP/MSP485714edhg0e7d9e6fi500003d336g6gccgffg6i?MSPStoreType=image/gif&s=5", + "alt": "Pie chart", + "title": "", + "width": 60, + "height": 62, + "type": "Default", + "themes": "1,2,3,4,5,6,7,8,9,10,11,12", + "colorinvertable": true + }, + "plaintext": "" + } + ], + "expressiontypes": { + "name": "Default" + } + }, + { + "title": "Continued fraction", + "scanner": "ContinuedFraction", + "id": "ContinuedFraction", + "position": 700, + "error": false, + "numsubpods": 1, + "subpods": [ + { + "title": "", + "img": { + "src": "https://www4c.wolframalpha.com/Calculate/MSP/MSP485814edhg0e7d9e6fi500001d7g1ciibagii80b?MSPStoreType=image/gif&s=5", + "alt": "[0; 2]", + "title": "[0; 2]", + "width": 548, + "height": 19, + "type": "Default", + "themes": "1,2,3,4,5,6,7,8,9,10,11,12", + "colorinvertable": true + }, + "plaintext": "[0; 2]" + } + ], + "expressiontypes": { + "name": "Default" + }, + "states": [ + { + "name": "Fraction form", + "input": "ContinuedFraction__Fraction form" + } + ] + } + ], + "assumptions": { + "type": "Clash", + "word": "5 / 10", + "template": "Assuming \"${word}\" is ${desc1}. Use as ${desc2} instead", + "count": 2, + "values": [ + { + "name": "FracNumber", + "desc": " referring to math", + "input": "*C.5+%2F+10-_*FracNumber-" + }, + { + "name": "DateObject", + "desc": "a date", + "input": "*C.5+%2F+10-_*DateObject-" + } + ] + } + } +} diff --git a/tests/resources/wolfram_alpha_api_response_with_no_primaries.json b/tests/resources/wolfram_alpha_api_response_with_no_primaries.json new file mode 100644 index 0000000..6b3b643 --- /dev/null +++ b/tests/resources/wolfram_alpha_api_response_with_no_primaries.json @@ -0,0 +1,300 @@ +{ + "queryresult": { + "success": true, + "error": false, + "numpods": 8, + "datatypes": "", + "timedout": "", + "timedoutpods": "", + "timing": 3.658, + "parsetiming": 0.427, + "parsetimedout": false, + "recalculate": "", + "id": "MSP444231b2g3hi9dh4e7200002c8f4f93hd7e1534", + "host": "https://www3.wolframalpha.com", + "server": "10", + "related": "https://www3.wolframalpha.com/api/v1/relatedQueries.jsp?id=MSPa445231b2g3hi9dh4e720000400770heb2hg9i7g9176140364258442551", + "version": "2.6", + "inputstring": "what is a url", + "pods": [ + { + "title": "Input interpretation", + "scanner": "Identity", + "id": "Input", + "position": 100, + "error": false, + "numsubpods": 1, + "subpods": [ + { + "title": "", + "img": { + "src": "https://www3.wolframalpha.com/Calculate/MSP/MSP446231b2g3hi9dh4e72000046954560094a96de?MSPStoreType=image/gif&s=10", + "alt": "URL (Wolfram Language symbol)", + "title": "URL (Wolfram Language symbol)", + "width": 223, + "height": 19, + "type": "Default", + "themes": "1,2,3,4,5,6,7,8,9,10,11,12", + "colorinvertable": true + }, + "plaintext": "URL (Wolfram Language symbol)" + } + ], + "expressiontypes": { + "name": "Default" + } + }, + { + "title": "Usage", + "scanner": "Data", + "id": "UsagePod:WolframLanguageData", + "position": 200, + "error": false, + "numsubpods": 1, + "subpods": [ + { + "title": "", + "img": { + "src": "https://www3.wolframalpha.com/Calculate/MSP/MSP447231b2g3hi9dh4e7200001265a4a7ah5i8467?MSPStoreType=image/gif&s=10", + "alt": "URL[\"url\"] is a symbolic representation of a URL.", + "title": "URL[\"url\"] is a symbolic representation of a URL.", + "width": 321, + "height": 19, + "type": "Grid", + "themes": "1,2,3,4,5,6,7,8,9,10,11,12", + "colorinvertable": true + }, + "plaintext": "URL[\"url\"] is a symbolic representation of a URL." + } + ], + "expressiontypes": { + "name": "Default" + }, + "infos": { + "links": { + "url": "http://reference.wolfram.com/language/ref/URL.html", + "text": "More information" + } + } + }, + { + "title": "Basic examples", + "scanner": "Data", + "id": "ExamplesPod:WolframLanguageData", + "position": 300, + "error": false, + "numsubpods": 1, + "subpods": [ + { + "title": "", + "img": { + "src": "https://www3.wolframalpha.com/Calculate/MSP/MSP448231b2g3hi9dh4e720000652fe8bedb0ega50?MSPStoreType=image/gif&s=10", + "alt": "Basic examples", + "title": "", + "width": 413, + "height": 247, + "type": "Default", + "themes": "1,2,3,4,5,6,7,8,9,10,11,12", + "colorinvertable": true + }, + "plaintext": "" + } + ], + "expressiontypes": { + "name": "Default" + } + }, + { + "title": "Attributes", + "scanner": "Data", + "id": "AttributesPod:WolframLanguageData", + "position": 400, + "error": false, + "numsubpods": 1, + "subpods": [ + { + "title": "", + "img": { + "src": "https://www3.wolframalpha.com/Calculate/MSP/MSP449231b2g3hi9dh4e7200002ccd5b8f44i414cd?MSPStoreType=image/gif&s=10", + "alt": "Protected | ReadProtected", + "title": "Protected | ReadProtected", + "width": 198, + "height": 19, + "type": "Grid", + "themes": "1,2,3,4,5,6,7,8,9,10,11,12", + "colorinvertable": true + }, + "plaintext": "Protected | ReadProtected" + } + ], + "expressiontypes": { + "name": "Default" + } + }, + { + "title": "Relationships with other entities", + "scanner": "Data", + "id": "RelatedEntitiesPod:WolframLanguageData", + "position": 500, + "error": false, + "numsubpods": 2, + "subpods": [ + { + "title": "Related symbols", + "img": { + "src": "https://www3.wolframalpha.com/Calculate/MSP/MSP450231b2g3hi9dh4e7200005556cc653e32ddfg?MSPStoreType=image/gif&s=10", + "alt": "URLRead | URLExecute | URLParse | URLBuild | Hyperlink | CloudObject | File | CopyFile | RemoteFile", + "title": "URLRead | URLExecute | URLParse | URLBuild | Hyperlink | CloudObject | File | CopyFile | RemoteFile", + "width": 475, + "height": 40, + "type": "Grid", + "themes": "1,2,3,4,5,6,7,8,9,10,11,12", + "colorinvertable": true + }, + "plaintext": "URLRead | URLExecute | URLParse | URLBuild | Hyperlink | CloudObject | File | CopyFile | RemoteFile" + }, + { + "title": "", + "img": { + "src": "https://www3.wolframalpha.com/Calculate/MSP/MSP451231b2g3hi9dh4e72000031f9ac66460f5g41?MSPStoreType=image/gif&s=10", + "alt": "Relationships with other entities", + "title": "", + "width": 440, + "height": 376, + "type": "Default", + "themes": "1,2,3,4,5,6,7,8,9,10,11,12", + "colorinvertable": true + }, + "plaintext": "" + } + ], + "expressiontypes": [ + { + "name": "Default" + }, + { + "name": "Default" + } + ], + "states": [ + { + "name": "Hide relationship communities", + "input": "RelatedEntitiesPod:WolframLanguageData__Hide relationship communities" + } + ] + }, + { + "title": "Typical ranks of usage in programs", + "scanner": "Data", + "id": "RanksPod:WolframLanguageData", + "position": 600, + "error": false, + "numsubpods": 2, + "subpods": [ + { + "title": "Frequency of usage in all corpuses", + "img": { + "src": "https://www3.wolframalpha.com/Calculate/MSP/MSP452231b2g3hi9dh4e720000643g1a5fc13b7ec5?MSPStoreType=image/gif&s=10", + "alt": "1727th most common (1 in 362000 symbols)", + "title": "1727th most common (1 in 362000 symbols)", + "width": 271, + "height": 22, + "type": "Default", + "themes": "1,2,3,4,5,6,7,8,9,10,11,12", + "colorinvertable": true + }, + "plaintext": "1727th most common (1 in 362000 symbols)" + }, + { + "title": "Rank of usage in the Wolfram documentation", + "img": { + "src": "https://www3.wolframalpha.com/Calculate/MSP/MSP453231b2g3hi9dh4e7200000g58105h71d404ig?MSPStoreType=image/gif&s=10", + "alt": "898th most common (1 in 16500 symbols)", + "title": "898th most common (1 in 16500 symbols)", + "width": 256, + "height": 22, + "type": "Default", + "themes": "1,2,3,4,5,6,7,8,9,10,11,12", + "colorinvertable": true + }, + "plaintext": "898th most common (1 in 16500 symbols)" + } + ], + "expressiontypes": [ + { + "name": "Default" + }, + { + "name": "Default" + } + ], + "states": [ + { + "name": "Show more ranks", + "input": "RanksPod:WolframLanguageData__Show more ranks" + } + ] + }, + { + "title": "History", + "scanner": "Data", + "id": "HistoryPod:WolframLanguageData", + "position": 700, + "error": false, + "numsubpods": 1, + "subpods": [ + { + "title": "Version introduced or modified", + "img": { + "src": "https://www3.wolframalpha.com/Calculate/MSP/MSP454231b2g3hi9dh4e72000020hb6gd0ga7g319c?MSPStoreType=image/gif&s=10", + "alt": "introduced in Version 11 (August 2016)\nlast modified in Version 12 (April 2019)", + "title": "introduced in Version 11 (August 2016)\nlast modified in Version 12 (April 2019)", + "width": 260, + "height": 46, + "type": "Grid", + "themes": "1,2,3,4,5,6,7,8,9,10,11,12", + "colorinvertable": true + }, + "plaintext": "introduced in Version 11 (August 2016)\nlast modified in Version 12 (April 2019)" + } + ], + "expressiontypes": { + "name": "Grid" + } + }, + { + "title": "Timeline", + "scanner": "Data", + "id": "TimelinePod:WolframLanguageData", + "position": 800, + "error": false, + "numsubpods": 1, + "subpods": [ + { + "title": "", + "img": { + "src": "https://www3.wolframalpha.com/Calculate/MSP/MSP455231b2g3hi9dh4e7200002i8ha7aei9ce325g?MSPStoreType=image/gif&s=10", + "alt": "Timeline", + "title": "", + "width": 550, + "height": 38, + "type": "Default", + "themes": "1,2,3,4,5,6,7,8,9,10,11,12", + "colorinvertable": true + }, + "plaintext": "" + } + ], + "expressiontypes": { + "name": "Default" + }, + "states": [ + { + "name": "Show release numbers", + "input": "TimelinePod:WolframLanguageData__Show release numbers" + } + ] + } + ] + } +} |
