Initial Commit
This commit is contained in:
156
internal/cards/calc.go
Normal file
156
internal/cards/calc.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package cards
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const calcExtraHead = `
|
||||
<link rel="stylesheet" href="/static/katex.min.css">
|
||||
<script defer src="/static/katex.min.js"></script>
|
||||
|
||||
<script src="/static/nerdamer.core.js"></script>
|
||||
<script src="/static/Algebra.js"></script>
|
||||
<script src="/static/Calculus.js"></script>
|
||||
<script src="/static/Solve.js"></script>`
|
||||
|
||||
const solveRenderScript = `
|
||||
<div id="calc-content" class="subtitle mx-2 my-0"></div>
|
||||
<script>
|
||||
window.onload = () => {
|
||||
latex = nerdamer.convertToLaTeX(nerdamer('%s').toString())
|
||||
katex.render(latex, document.getElementById('calc-content'))
|
||||
}
|
||||
</script>`
|
||||
|
||||
func init() {
|
||||
// Register calc card
|
||||
Register("calc", 0, NewCalcCard)
|
||||
}
|
||||
|
||||
// CalcCard represents a calculation card
|
||||
type CalcCard struct {
|
||||
query string
|
||||
useFunc bool
|
||||
function string
|
||||
args []string
|
||||
expression string
|
||||
}
|
||||
|
||||
// NewCalcCard is a NewCardFunc that creates a new CalcCard
|
||||
func NewCalcCard(query, _ string) Card {
|
||||
return &CalcCard{query: query, useFunc: true}
|
||||
}
|
||||
|
||||
func (cc *CalcCard) Matches() bool {
|
||||
// If query has solve prefix
|
||||
if strings.HasPrefix(cc.query, "solve") {
|
||||
// Set function to solve
|
||||
cc.function = "solve"
|
||||
|
||||
// Compile regex for specifying veriable to solve for
|
||||
forRgx := regexp.MustCompile(`solve (.+) for (.+)`)
|
||||
// If query matches regex
|
||||
if forRgx.MatchString(cc.query) {
|
||||
// Find furst regex match
|
||||
matches := forRgx.FindStringSubmatch(cc.query)
|
||||
// Append function aeguments. First is equation, second variable
|
||||
cc.args = append(cc.args, matches[1], matches[2])
|
||||
} else {
|
||||
// Append function arguments assuming variable is x
|
||||
cc.args = append(cc.args, cc.StripKey(), "x")
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// If query has integrate prefix
|
||||
if strings.HasPrefix(cc.query, "integrate") ||
|
||||
strings.HasPrefix(cc.query, "integral of") ||
|
||||
strings.HasPrefix(cc.query, "integral") {
|
||||
|
||||
// Set function to integrate
|
||||
cc.function = "integrate"
|
||||
// Append function arguments
|
||||
cc.args = append(cc.args, cc.StripKey())
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// If query has derivative prefix
|
||||
if strings.HasPrefix(cc.query, "diff") ||
|
||||
strings.HasPrefix(cc.query, "derive") ||
|
||||
strings.HasPrefix(cc.query, "differentiate") ||
|
||||
strings.HasPrefix(cc.query, "derivative of") ||
|
||||
strings.HasPrefix(cc.query, "derivative") {
|
||||
|
||||
// Set function to diff
|
||||
cc.function = "diff"
|
||||
// Append function arguments
|
||||
cc.args = append(cc.args, cc.StripKey())
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// If query has calculate prefix
|
||||
if strings.HasPrefix(cc.query, "calculate") {
|
||||
// This is an expression, so no function needed
|
||||
cc.useFunc = false
|
||||
// Set expression to query stripped of keys
|
||||
cc.expression = cc.StripKey()
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (cc *CalcCard) StripKey() string {
|
||||
// Compile regex for words to be removed
|
||||
trimRgx := regexp.MustCompile(`^(.*?)solve|integrate|integral(?: of)?|diff|differentiate|derivative(?: of)?|derive|calculate(.*)$`)
|
||||
// Return string with words removed
|
||||
return trimRgx.ReplaceAllString(cc.query, "${1}${2}")
|
||||
}
|
||||
|
||||
func (cc *CalcCard) Content() template.HTML {
|
||||
var input string
|
||||
// If function is being used
|
||||
if cc.useFunc {
|
||||
// Set input to formatted function
|
||||
input = formatFunc(cc.function, cc.args)
|
||||
} else {
|
||||
// Set input to expression
|
||||
input = cc.expression
|
||||
}
|
||||
// Return script with given input
|
||||
return template.HTML(fmt.Sprintf(
|
||||
solveRenderScript,
|
||||
input,
|
||||
))
|
||||
}
|
||||
|
||||
func (cc *CalcCard) Footer() template.HTML {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (cc *CalcCard) Returned() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (cc *CalcCard) RunQuery() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cc *CalcCard) Title() string {
|
||||
return "Calculation"
|
||||
}
|
||||
|
||||
func (cc *CalcCard) Head() template.HTML {
|
||||
return calcExtraHead
|
||||
}
|
||||
|
||||
func formatFunc(function string, args []string) string {
|
||||
// Format as function(arg1,arg2...)
|
||||
return function + "(" + strings.Join(args, ",") + ")"
|
||||
}
|
||||
88
internal/cards/cards.go
Normal file
88
internal/cards/cards.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package cards
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"html/template"
|
||||
"sort"
|
||||
)
|
||||
|
||||
var ErrCardRegistered = errors.New("card with that name has already been registered")
|
||||
|
||||
// cardRegistration represents a card that has been registered
|
||||
type cardRegistration struct {
|
||||
name string
|
||||
priority int
|
||||
newFn NewCardFunc
|
||||
}
|
||||
|
||||
// cards stores all registered cards
|
||||
var cards = []cardRegistration{}
|
||||
|
||||
// NewCardFunc creates and returns a new card.
|
||||
// This should not be an expensive operation
|
||||
type NewCardFunc func(query, userAgent string) Card
|
||||
|
||||
// Card represents a search result card
|
||||
type Card interface {
|
||||
// RunQuery runs any HTTP or other requests
|
||||
RunQuery() error
|
||||
|
||||
// Returned returns whether the query
|
||||
// returned any information
|
||||
Returned() bool
|
||||
|
||||
// Matches returns whether the query matches
|
||||
// for the card
|
||||
Matches() bool
|
||||
|
||||
// StripKey removes the key words and returns
|
||||
// the updated query
|
||||
StripKey() string
|
||||
|
||||
// Title returns the card title
|
||||
Title() string
|
||||
|
||||
// Content returns the contents of the card
|
||||
Content() template.HTML
|
||||
|
||||
// Footer returns the contents of the card footer
|
||||
Footer() template.HTML
|
||||
|
||||
// Head returns any extras to include in <head>
|
||||
Head() template.HTML
|
||||
}
|
||||
|
||||
// Register adds a new card to the library
|
||||
func Register(name string, priority int, fn NewCardFunc) {
|
||||
// For every registered card
|
||||
for _, cardReg := range cards {
|
||||
// If priority already exists, increase
|
||||
if cardReg.priority == priority {
|
||||
priority++
|
||||
}
|
||||
// If card already registered, return
|
||||
if cardReg.name == name {
|
||||
return
|
||||
}
|
||||
}
|
||||
// Add card to slice
|
||||
cards = append(cards, cardRegistration{name, priority, fn})
|
||||
}
|
||||
|
||||
// GetCard returns a matching registered card
|
||||
func GetCard(query, userAgent string) Card {
|
||||
// Sort slice by priority
|
||||
sort.Slice(cards, func(i, j int) bool {
|
||||
return cards[i].priority < cards[j].priority
|
||||
})
|
||||
// For every registered card
|
||||
for _, cardReg := range cards {
|
||||
// Create new card
|
||||
card := cardReg.newFn(query, userAgent)
|
||||
// If card matches, return it
|
||||
if card.Matches() {
|
||||
return card
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
115
internal/cards/ddg.go
Normal file
115
internal/cards/ddg.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package cards
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("ddg", 3, NewDDGCard)
|
||||
}
|
||||
|
||||
// ddgAttribution is HTML for attribution of DuckDuckGo at their request
|
||||
const ddgAttribution = `<p class="is-size-7 pt-2">
|
||||
<span class="iconify" data-icon="simple-icons:duckduckgo" data-inline="true"></span>
|
||||
Results from
|
||||
<a href="https://duckduckgo.com/%s">DuckDuckGo »</a>
|
||||
</p>`
|
||||
|
||||
// DDGCard represents a DuckDuckGo Instant Answer API card
|
||||
type DDGCard struct {
|
||||
query string
|
||||
userAgent string
|
||||
resp DDGInstAns
|
||||
}
|
||||
|
||||
// DDGInstAns represents a DuckDuckGo Instant Answer API response
|
||||
type DDGInstAns struct {
|
||||
Abstract string
|
||||
AbstractText string
|
||||
AbstractSource string
|
||||
AbstractURL string
|
||||
Image string
|
||||
Heading string
|
||||
Answer string
|
||||
AnswerType string
|
||||
}
|
||||
|
||||
// NewDDGCard isa NewCardFunc that creates a new DDGCard
|
||||
func NewDDGCard(query, userAgent string) Card {
|
||||
return &DDGCard{
|
||||
query: url.QueryEscape(query),
|
||||
userAgent: userAgent,
|
||||
}
|
||||
}
|
||||
|
||||
// RunQuery requests the query from the instant answer API
|
||||
func (ddg *DDGCard) RunQuery() error {
|
||||
http.DefaultClient.Timeout = 5 * time.Second
|
||||
|
||||
// Create new API request
|
||||
req, err := http.NewRequest(
|
||||
http.MethodGet,
|
||||
"https://api.duckduckgo.com/?q="+ddg.query+"&format=json",
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("User-Agent", ddg.userAgent)
|
||||
|
||||
// Perform request
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Decode response into repsonse struct
|
||||
err = json.NewDecoder(res.Body).Decode(&ddg.resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ddg *DDGCard) Returned() bool {
|
||||
// Value was returned if abstract is not empty
|
||||
return ddg.resp.Abstract != ""
|
||||
}
|
||||
|
||||
func (ddg *DDGCard) Matches() bool {
|
||||
// Everything matches since there are no keys
|
||||
return true
|
||||
}
|
||||
|
||||
func (ddg *DDGCard) StripKey() string {
|
||||
// No key to strip, so return query
|
||||
return ddg.query
|
||||
}
|
||||
|
||||
func (ddg *DDGCard) Title() string {
|
||||
return ddg.resp.Heading
|
||||
}
|
||||
|
||||
func (ddg *DDGCard) Content() template.HTML {
|
||||
// Return abstract with attribution
|
||||
return template.HTML(ddg.resp.Abstract + fmt.Sprintf(ddgAttribution, ddg.query))
|
||||
}
|
||||
|
||||
func (ddg *DDGCard) Footer() template.HTML {
|
||||
// Return footer with abstract url and source
|
||||
return template.HTML(fmt.Sprintf(
|
||||
`<div class="card-footer"><a class="card-footer-item" href="%s">%s</a></div>`,
|
||||
ddg.resp.AbstractURL,
|
||||
ddg.resp.AbstractSource,
|
||||
))
|
||||
}
|
||||
|
||||
func (ddg *DDGCard) Head() template.HTML {
|
||||
return ""
|
||||
}
|
||||
247
internal/cards/metaweather.go
Normal file
247
internal/cards/metaweather.go
Normal file
@@ -0,0 +1,247 @@
|
||||
package cards
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bcicen/go-units"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("metaweather", 1, NewMetaweatherCard)
|
||||
}
|
||||
|
||||
const metaweatherContent = `
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<span class="iconify" data-width="75" data-height="75" data-icon="%s"></span><p class="subtitle">%s</p>
|
||||
</div>
|
||||
<div class="column has-text-right">
|
||||
<p class="title has-text-weight-normal">%s</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column has-text-centered">
|
||||
<p><b>Min Temp:</b> %s</p>
|
||||
<p><b>Max Temp:</b> %s</p>
|
||||
<p><b>Wind Speed:</b> %s</p>
|
||||
</div>
|
||||
<div class="column has-text-centered">
|
||||
<p><b>Humidity:</b> %d%%</p>
|
||||
<p><b>Visibility:</b> %s</p>
|
||||
<p><b>Predictability:</b> %d%%</p>
|
||||
</div>
|
||||
<div class="column has-text-centered">
|
||||
<p><b>Wind Direction:</b> %s</p>
|
||||
<p><b>Location:</b> %s</p>
|
||||
<p><b>Timezone:</b> %s</p>
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
// weatherIconMap maps metaweather abbreviations
|
||||
// to iconify icons
|
||||
var weatherIconMap = map[string]string{
|
||||
"sn": "bi:cloud-snow",
|
||||
"sl": "bi:cloud-sleet",
|
||||
"h": "bi:cloud-hail",
|
||||
"t": "bi:cloud-lightning-rain",
|
||||
"hr": "bi:cloud-rain-heavy",
|
||||
"lr": "bi:cloud-drizzle",
|
||||
"s": "wi:day-rain",
|
||||
"hc": "bi:cloud-fill",
|
||||
"lc": "bi:cloud",
|
||||
"c": "akar-icons:sun",
|
||||
}
|
||||
|
||||
// MetaweatherCard represents a metaweather card
|
||||
type MetaweatherCard struct {
|
||||
query string
|
||||
userAgent string
|
||||
location []MetaweatherLocation
|
||||
resp MetaweatherResponse
|
||||
}
|
||||
|
||||
// NewMetaweatherCard is a NewCardFunc that creates a new MetaweatherCard
|
||||
func NewMetaweatherCard(query, userAgent string) Card {
|
||||
return &MetaweatherCard{
|
||||
query: query,
|
||||
userAgent: userAgent,
|
||||
}
|
||||
}
|
||||
|
||||
// MetaweatherLocation represents a location
|
||||
type MetaweatherLocation struct {
|
||||
Title string `json:"title"`
|
||||
WOEID int `json:"woeid"`
|
||||
}
|
||||
|
||||
// MetaweatherResponse represents a response from
|
||||
// the metaweather API
|
||||
type MetaweatherResponse struct {
|
||||
Detail string `json:"detail"`
|
||||
Title string `json:"title"`
|
||||
Timezone string `json:"timezone"`
|
||||
Consolidated []struct {
|
||||
ID int `json:"id"`
|
||||
State string `json:"weather_state_name"`
|
||||
StateAbbr string `json:"weather_state_abbr"`
|
||||
WindCompass string `json:"wind_direction_compass"`
|
||||
MinTemp float64 `json:"min_temp"`
|
||||
MaxTemp float64 `json:"max_temp"`
|
||||
CurrentTemp float64 `json:"the_temp"`
|
||||
WindSpeed float64 `json:"wind_speed"`
|
||||
WindDirection float64 `json:"wind_direction"`
|
||||
AirPressure float64 `json:"air_pressure"`
|
||||
Visibility float64 `json:"visibility"`
|
||||
Humidity int `json:"humidity"`
|
||||
Predictability int `json:"predictability"`
|
||||
} `json:"consolidated_weather"`
|
||||
}
|
||||
|
||||
// RunQuery searches for the location and then runs an API query
|
||||
// using the returned WOEID
|
||||
func (mc *MetaweatherCard) RunQuery() error {
|
||||
http.DefaultClient.Timeout = 5 * time.Second
|
||||
|
||||
// Create location search request
|
||||
req, err := http.NewRequest(
|
||||
http.MethodGet,
|
||||
"https://www.metaweather.com/api/location/search/?query="+url.QueryEscape(mc.StripKey()),
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("User-Agent", mc.userAgent)
|
||||
|
||||
// Perform request
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Decode response into location
|
||||
err = json.NewDecoder(res.Body).Decode(&mc.location)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res.Body.Close()
|
||||
|
||||
// If no locations, search returned no results
|
||||
if len(mc.location) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create request for weather data
|
||||
req, err = http.NewRequest(
|
||||
http.MethodGet,
|
||||
"https://www.metaweather.com/api/location/"+strconv.Itoa(mc.location[0].WOEID),
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("User-Agent", mc.userAgent)
|
||||
|
||||
// Perform request
|
||||
res, err = http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Decode response into response struct
|
||||
err = json.NewDecoder(res.Body).Decode(&mc.resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mc *MetaweatherCard) Returned() bool {
|
||||
return len(mc.location) > 0 && mc.resp.Detail != "Not found."
|
||||
}
|
||||
|
||||
func (mc *MetaweatherCard) Matches() bool {
|
||||
return strings.HasPrefix(mc.query, "weather in") ||
|
||||
strings.HasPrefix(mc.query, "weather") ||
|
||||
strings.HasSuffix(mc.query, "weather")
|
||||
}
|
||||
|
||||
func (mc *MetaweatherCard) StripKey() string {
|
||||
query := strings.TrimPrefix(mc.query, "weather in")
|
||||
query = strings.TrimPrefix(query, "weather")
|
||||
query = strings.TrimSuffix(query, "weather")
|
||||
return strings.TrimSpace(query)
|
||||
}
|
||||
|
||||
func (mc *MetaweatherCard) Content() template.HTML {
|
||||
return template.HTML(fmt.Sprintf(
|
||||
metaweatherContent,
|
||||
// Write iconift icon for weather state
|
||||
weatherIconMap[mc.resp.Consolidated[0].StateAbbr],
|
||||
// Write weather state
|
||||
mc.resp.Consolidated[0].State,
|
||||
// Convert and write current temperature
|
||||
convert(mc.resp.Consolidated[0].CurrentTemp, "temperature", units.Celsius),
|
||||
// Convert and write minimum temperature
|
||||
convert(mc.resp.Consolidated[0].MinTemp, "temperature", units.Celsius),
|
||||
// Convert and write maximum temperature
|
||||
convert(mc.resp.Consolidated[0].MaxTemp, "temperature", units.Celsius),
|
||||
// Write wind speed
|
||||
fmt.Sprintf("%.2f mph", mc.resp.Consolidated[0].WindSpeed),
|
||||
// Write humidity percentafe
|
||||
mc.resp.Consolidated[0].Humidity,
|
||||
// Convert and write visibility
|
||||
convert(mc.resp.Consolidated[0].Visibility, "visibility", units.Mile),
|
||||
// Write predictability percentage
|
||||
mc.resp.Consolidated[0].Predictability,
|
||||
// Write compass wind direction
|
||||
mc.resp.Consolidated[0].WindCompass,
|
||||
// Write title
|
||||
mc.resp.Title,
|
||||
// Write timezone
|
||||
mc.resp.Timezone,
|
||||
))
|
||||
}
|
||||
|
||||
func (mc *MetaweatherCard) Title() string {
|
||||
return "Weather"
|
||||
}
|
||||
|
||||
func (mc *MetaweatherCard) Footer() template.HTML {
|
||||
// Return footer with link to metaweather
|
||||
return `<div class="card-footer"><a class="card-footer-item" href="https://www.metaweather.com/">Metaweather</a></div>`
|
||||
}
|
||||
|
||||
func (mc *MetaweatherCard) Head() template.HTML {
|
||||
return ""
|
||||
}
|
||||
|
||||
// convert converts a value to a unit specified in the config, and then formats it
|
||||
func convert(val float64, suffix string, defaultUnit units.Unit) string {
|
||||
// Find unit in config
|
||||
unit, err := units.Find(viper.GetString("search.cards.weather.units." + suffix))
|
||||
if err != nil {
|
||||
unit = defaultUnit
|
||||
}
|
||||
// Convert value to specified unit
|
||||
newVal, err := units.ConvertFloat(val, defaultUnit, unit)
|
||||
opts := units.FmtOptions{
|
||||
Label: true,
|
||||
Short: true,
|
||||
Precision: 2,
|
||||
}
|
||||
if err != nil {
|
||||
return units.NewValue(val, defaultUnit).Fmt(opts)
|
||||
}
|
||||
return newVal.Fmt(opts)
|
||||
}
|
||||
89
internal/cards/plot.go
Normal file
89
internal/cards/plot.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package cards
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const plotExtraHead = `
|
||||
<script src="/static/function-plot.js"></script>
|
||||
<style>
|
||||
.top-right-legend {
|
||||
display: none;
|
||||
}
|
||||
</style>`
|
||||
|
||||
const plotScript = `
|
||||
<div id="plot-content" class="container"></div>
|
||||
<script>
|
||||
plotFn = () => functionPlot({
|
||||
target: '#plot-content',
|
||||
grid: true,
|
||||
width: document.getElementById('plot-content').offsetWidth,
|
||||
data: [{
|
||||
fn: '%s'
|
||||
}]
|
||||
})
|
||||
new ResizeObserver(plotFn).observe(document.getElementById('plot-content'))
|
||||
</script>`
|
||||
|
||||
func init() {
|
||||
// Register plot card
|
||||
Register("plot", 2, NewPlotCard)
|
||||
}
|
||||
|
||||
// PlotCard represents a card with an equation plot
|
||||
type PlotCard struct {
|
||||
query string
|
||||
}
|
||||
|
||||
// NewPlotCard is a NewCardFunc that creates a new PlotCard
|
||||
func NewPlotCard(query, _ string) Card {
|
||||
return &PlotCard{query: query}
|
||||
}
|
||||
|
||||
func (pc *PlotCard) Matches() bool {
|
||||
return strings.HasPrefix(pc.query, "plot") ||
|
||||
strings.HasPrefix(pc.query, "graph") ||
|
||||
strings.HasPrefix(pc.query, "draw")
|
||||
}
|
||||
|
||||
func (pc *PlotCard) StripKey() string {
|
||||
query := strings.TrimPrefix(pc.query, "plot")
|
||||
query = strings.TrimPrefix(query, "graph")
|
||||
query = strings.TrimPrefix(query, "draw")
|
||||
return strings.TrimSpace(query)
|
||||
}
|
||||
|
||||
func (pc *PlotCard) Content() template.HTML {
|
||||
return template.HTML(fmt.Sprintf(
|
||||
plotScript,
|
||||
pc.StripKey(),
|
||||
))
|
||||
}
|
||||
|
||||
// Since this card is frontend, this cannot be checked.
|
||||
// Therefore, it will always return true.
|
||||
func (pc *PlotCard) Returned() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (pc *PlotCard) Title() string {
|
||||
return "Plot (" + pc.StripKey() + ")"
|
||||
}
|
||||
|
||||
func (pc *PlotCard) Head() template.HTML {
|
||||
return plotExtraHead
|
||||
}
|
||||
|
||||
func (pc *PlotCard) Footer() template.HTML {
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
func (pc *PlotCard) RunQuery() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user