textcore

package
v0.3.13 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 6, 2026 License: BSD-3-Clause Imports: 54 Imported by: 6

README

textcore: core GUI widgets for text

Textcore has various text-based core.Widgets, including:

  • Base is a base implementation that views lines.Lines text content.
  • Editor is a full-featured text editor built on Base.
  • DiffEditor provides side-by-side Editors showing the diffs between files.
  • TwinEditors provides two side-by-side Editors that sync their scrolling.
  • Terminal provides a VT100-style terminal built on Base (TODO).

A critical design feature is that the Base widget can switch efficiently among different lines.Lines content. For example, in Cogent Code there are 2 editor widgets that are reused for all files, including viewing the same file across both editors. Thus, all of the state comes from the underlying Lines buffers.

The Lines handles all layout and markup styling, so the Base just renders the results of that. Thus, there is no need for the Editor to ever drive a NeedsLayout call itself: everything is handled in the Render step, including the presence or absence of the scrollbar, which is a little bit complicated because adding a scrollbar changes the effective width and thus the internal layout.

Files

The underlying lines.Lines object does not have any core dependencies, and is designed to manage lines in memory. See files.go for standard functions to provide GUI-based interactions for prompting when a file has been modified on a disk, and prompts for saving a file. These functions can be used on a Lines without any specific widget.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var PrevISearchString string

PrevISearchString is the previous ISearch string

Functions

func Close

func Close(sc *core.Scene, lns *lines.Lines, afterFunc func(canceled bool)) bool

Close closes the lines viewed by this editor, prompting to save if there are changes. If afterFunc is non-nil, then it is called with the status of the user action. Returns false if the file was actually not closed pending input from the user.

func FileModPrompt

func FileModPrompt(sc *core.Scene, lns *lines.Lines) bool

FileModPrompt is called when a file has been modified in the filesystem and it is about to be modified through an edit, in the fileModCheck function. The prompt determines whether the user wants to revert, overwrite, or save current version as a different file.

func Save

func Save(sc *core.Scene, lns *lines.Lines) error

Save saves the given Lines into the current filename associated with this buffer, prompting if the file is changed on disk since the last save.

func SaveAs

func SaveAs(sc *core.Scene, lns *lines.Lines, filename string, afterFunc func(canceled bool))

SaveAs saves the given Lines text into the given file. Checks for an existing file: If it does exist then prompts to overwrite or not. If afterFunc is non-nil, then it is called with the status of the user action.

Types

type Base

type Base struct {
	core.Frame

	// Lines is the text lines content for this editor.
	Lines *lines.Lines `set:"-" json:"-" xml:"-"`

	// CursorWidth is the width of the cursor.
	// This should be set in Stylers like all other style properties.
	CursorWidth units.Value

	// LineNumberColor is the color used for the side bar containing the line numbers.
	// This should be set in Stylers like all other style properties.
	LineNumberColor image.Image

	// SelectColor is the color used for the user text selection background color.
	// This should be set in Stylers like all other style properties.
	SelectColor image.Image

	// HighlightColor is the color used for the text highlight background color (like in find).
	// This should be set in Stylers like all other style properties.
	HighlightColor image.Image

	// CursorColor is the color used for the text editor cursor bar.
	// This should be set in Stylers like all other style properties.
	CursorColor image.Image

	// AutoscrollOnInput scrolls the display to the end when Input events are received.
	AutoscrollOnInput bool

	// CursorPos is the current cursor position.
	CursorPos textpos.Pos `set:"-" edit:"-" json:"-" xml:"-"`

	// SelectRegion is the current selection region.
	SelectRegion textpos.Region `set:"-" edit:"-" json:"-" xml:"-"`

	// Highlights is a slice of regions representing the highlighted
	// regions, e.g., for search results.
	Highlights []textpos.Region `set:"-" edit:"-" json:"-" xml:"-"`

	// LinkHandler handles link clicks.
	// If it is nil, they are sent to the standard web URL handler.
	LinkHandler func(tl *rich.Hyperlink)
	// contains filtered or unexported fields
}

Base is a widget with basic infrastructure for viewing and editing lines.Lines of monospaced text, used in textcore.Editor and terminal. There can be multiple Base widgets for each lines buffer.

Use NeedsRender to drive an render update for any change that does not change the line-level layout of the text.

All updating in the Base should be within a single goroutine, as it would require extensive protections throughout code otherwise.

func AsBase

func AsBase(n tree.Node) *Base

AsBase returns the given value as a value of type Base if the type of the given value embeds Base, or nil otherwise

func NewBase

func NewBase(parent ...tree.Node) *Base

NewBase returns a new Base with the given optional parent: Base is a widget with basic infrastructure for viewing and editing lines.Lines of monospaced text, used in textcore.Editor and terminal. There can be multiple Base widgets for each lines buffer.

Use NeedsRender to drive an render update for any change that does not change the line-level layout of the text.

All updating in the Base should be within a single goroutine, as it would require extensive protections throughout code otherwise.

func (*Base) ApplyScenePos

func (ed *Base) ApplyScenePos()

func (*Base) AsBase

func (t *Base) AsBase() *Base

AsBase satisfies the BaseEmbedder interface

func (*Base) Clear

func (ed *Base) Clear()

Clear resets all the text in the buffer for this editor.

func (*Base) Copy

func (ed *Base) Copy(reset bool) *textpos.Edit

Copy copies any selected text to the clipboard, and returns that text, optionally resetting the current selection

func (*Base) CopyRect

func (ed *Base) CopyRect(reset bool) *textpos.Edit

CopyRect copies any selected text to the clipboard, and returns that text, optionally resetting the current selection

func (ed *Base) CursorNextLink(wraparound bool) bool

CursorNextLink moves cursor to next link. wraparound wraps around to top of buffer if none found -- returns true if found

func (ed *Base) CursorPrevLink(wraparound bool) bool

CursorPrevLink moves cursor to next link. wraparound wraps around to bottom of buffer if none found -- returns true if found

func (*Base) CursorStartDoc

func (ed *Base) CursorStartDoc()

CursorStartDoc moves the cursor to the start of the text, updating selection if select mode is active

func (*Base) CursorToHistoryNext

func (ed *Base) CursorToHistoryNext() bool

CursorToHistoryNext moves cursor to previous position on history list -- returns true if moved

func (*Base) CursorToHistoryPrev

func (ed *Base) CursorToHistoryPrev() bool

CursorToHistoryPrev moves cursor to previous position on history list. returns true if moved

func (*Base) Cut

func (ed *Base) Cut() *textpos.Edit

Cut cuts any selected text and adds it to the clipboard, also returns cut text

func (*Base) CutRect

func (ed *Base) CutRect() *textpos.Edit

CutRect cuts rectangle defined by selected text (upper left to lower right) and adds it to the clipboard, also returns cut lines.

func (*Base) Destroy

func (ed *Base) Destroy()

func (*Base) HasSelection

func (ed *Base) HasSelection() bool

HasSelection returns whether there is a selected region of text

func (*Base) HighlightRegion

func (ed *Base) HighlightRegion(reg textpos.Region)

HighlightRegion adds a new highlighted region. Use HighlightsReset to clear any existing prior to this if only one region desired.

func (*Base) HighlightsReset

func (ed *Base) HighlightsReset()

HighlightsReset resets the list of all highlighted regions.

func (*Base) Init

func (ed *Base) Init()

func (*Base) InsertAtCursor

func (ed *Base) InsertAtCursor(txt []byte)

InsertAtCursor inserts given text at current cursor position

func (*Base) IsNotSaved

func (ed *Base) IsNotSaved() bool

IsNotSaved returns true if buffer was changed (edited) since last Save.

func (*Base) LineNumberPixels

func (ed *Base) LineNumberPixels() float32

func (*Base) NumLines

func (ed *Base) NumLines() int

func (*Base) OpenLinkAt

func (ed *Base) OpenLinkAt(pos textpos.Pos) (*rich.Hyperlink, int)

OpenLinkAt opens a link at given cursor position, if one exists there. returns the link if found, else nil. Also highlights the selected link.

func (*Base) Paste

func (ed *Base) Paste()

Paste inserts text from the clipboard at current cursor position

func (*Base) PasteRect

func (ed *Base) PasteRect()

PasteRect inserts text from the clipboard at current cursor position

func (*Base) PixelToCursor

func (ed *Base) PixelToCursor(pt image.Point) textpos.Pos

PixelToCursor finds the cursor position that corresponds to the given pixel location (e.g., from mouse click), in widget-relative coordinates.

func (*Base) Position

func (ed *Base) Position()

func (*Base) ReCaseSelection

func (ed *Base) ReCaseSelection(c strcase.Cases) string

ReCaseSelection changes the case of the currently selected lines. Returns the new text; empty if nothing selected.

func (*Base) RenderWidget

func (ed *Base) RenderWidget()

func (*Base) ScrollChanged

func (ed *Base) ScrollChanged(d math32.Dims, sb *core.Slider)

func (*Base) ScrollValues

func (ed *Base) ScrollValues(d math32.Dims) (maxSize, visSize, visPct float32)

func (*Base) SelectReset

func (ed *Base) SelectReset()

SelectReset resets the selection

func (*Base) Selection

func (ed *Base) Selection() *textpos.Edit

Selection returns the currently selected text as a textpos.Edit, which captures start, end, and full lines in between -- nil if no selection

func (*Base) SendClose

func (ed *Base) SendClose()

SendClose sends the events.Close event, when lines buffer is closed.

func (*Base) SendInput

func (ed *Base) SendInput()

SendInput sends the events.Input event, for fine-grained updates.

func (*Base) SetAutoscrollOnInput

func (t *Base) SetAutoscrollOnInput(v bool) *Base

SetAutoscrollOnInput sets the [Base.AutoscrollOnInput]: AutoscrollOnInput scrolls the display to the end when Input events are received.

func (*Base) SetCursorColor

func (t *Base) SetCursorColor(v image.Image) *Base

SetCursorColor sets the [Base.CursorColor]: CursorColor is the color used for the text editor cursor bar. This should be set in Stylers like all other style properties.

func (*Base) SetCursorShow

func (ed *Base) SetCursorShow(pos textpos.Pos)

SetCursorShow sets a new cursor position, enforcing it in range, and shows the cursor (scroll to if hidden, render)

func (*Base) SetCursorTarget

func (ed *Base) SetCursorTarget(pos textpos.Pos)

SetCursorTarget sets a new cursor target position, ensures that it is visible. Setting the textpos.PosErr value causes it to go the end of doc, the position of which may not be known at the time the target is set.

func (*Base) SetCursorWidth

func (t *Base) SetCursorWidth(v units.Value) *Base

SetCursorWidth sets the [Base.CursorWidth]: CursorWidth is the width of the cursor. This should be set in Stylers like all other style properties.

func (*Base) SetHighlightColor

func (t *Base) SetHighlightColor(v image.Image) *Base

SetHighlightColor sets the [Base.HighlightColor]: HighlightColor is the color used for the text highlight background color (like in find). This should be set in Stylers like all other style properties.

func (*Base) SetLineNumberColor

func (t *Base) SetLineNumberColor(v image.Image) *Base

SetLineNumberColor sets the [Base.LineNumberColor]: LineNumberColor is the color used for the side bar containing the line numbers. This should be set in Stylers like all other style properties.

func (*Base) SetLines

func (ed *Base) SetLines(ln *lines.Lines) *Base

SetLines sets the lines.Lines that this is an editor of, creating a new view for this editor and connecting to events.

func (*Base) SetLinkHandler

func (t *Base) SetLinkHandler(v func(tl *rich.Hyperlink)) *Base

SetLinkHandler sets the [Base.LinkHandler]: LinkHandler handles link clicks. If it is nil, they are sent to the standard web URL handler.

func (*Base) SetScrollParams

func (ed *Base) SetScrollParams(d math32.Dims, sb *core.Slider)

func (*Base) SetSelectColor

func (t *Base) SetSelectColor(v image.Image) *Base

SetSelectColor sets the [Base.SelectColor]: SelectColor is the color used for the user text selection background color. This should be set in Stylers like all other style properties.

func (*Base) SetWidgetValue

func (ed *Base) SetWidgetValue(value any) error

func (*Base) SizeDown

func (ed *Base) SizeDown(iter int) bool

func (*Base) SizeFinal

func (ed *Base) SizeFinal()

func (*Base) SizeUp

func (ed *Base) SizeUp()

func (*Base) Style

func (ed *Base) Style()

func (*Base) WidgetValue

func (ed *Base) WidgetValue() any

type BaseEmbedder

type BaseEmbedder interface {
	AsBase() *Base
}

BaseEmbedder is an interface that all types that embed Base satisfy

type DiffEditor

type DiffEditor struct {
	core.Frame

	// first file name being compared
	FileA string

	// second file name being compared
	FileB string

	// revision for first file, if relevant
	RevisionA string

	// revision for second file, if relevant
	RevisionB string
	// contains filtered or unexported fields
}

DiffEditor presents two side-by-side [Editor]s showing the differences between two files (represented as lines of strings).

func DiffEditorDialog

func DiffEditorDialog(ctx core.Widget, title string, astr, bstr []string, afile, bfile, arev, brev string) *DiffEditor

DiffEditorDialog opens a dialog for displaying diff between two files as line-strings

func DiffEditorDialogFromRevs

func DiffEditorDialogFromRevs(ctx core.Widget, repo vcs.Repo, file string, fbuf *lines.Lines, rev_a, rev_b string) (*DiffEditor, error)

DiffEditorDialogFromRevs opens a dialog for displaying diff between file at two different revisions from given repository if empty, defaults to: A = current HEAD, B = current WC file. -1, -2 etc also work as universal ways of specifying prior revisions.

func DiffFiles

func DiffFiles(ctx core.Widget, afile, bfile string) (*DiffEditor, error)

DiffFiles shows the diffs between this file as the A file, and other file as B file, in a DiffEditorDialog

func NewDiffEditor

func NewDiffEditor(parent ...tree.Node) *DiffEditor

NewDiffEditor returns a new DiffEditor with the given optional parent: DiffEditor presents two side-by-side [Editor]s showing the differences between two files (represented as lines of strings).

func (*DiffEditor) DiffStrings

func (dv *DiffEditor) DiffStrings(astr, bstr []string)

DiffStrings computes differences between two lines-of-strings and displays in DiffEditor.

func (*DiffEditor) Init

func (dv *DiffEditor) Init()

func (*DiffEditor) MakeToolbar

func (dv *DiffEditor) MakeToolbar(p *tree.Plan)

func (*DiffEditor) SetFileA

func (t *DiffEditor) SetFileA(v string) *DiffEditor

SetFileA sets the [DiffEditor.FileA]: first file name being compared

func (*DiffEditor) SetFileB

func (t *DiffEditor) SetFileB(v string) *DiffEditor

SetFileB sets the [DiffEditor.FileB]: second file name being compared

func (*DiffEditor) SetRevisionA

func (t *DiffEditor) SetRevisionA(v string) *DiffEditor

SetRevisionA sets the [DiffEditor.RevisionA]: revision for first file, if relevant

func (*DiffEditor) SetRevisionB

func (t *DiffEditor) SetRevisionB(v string) *DiffEditor

SetRevisionB sets the [DiffEditor.RevisionB]: revision for second file, if relevant

type DiffTextEditor

type DiffTextEditor struct {
	Editor
}

DiffTextEditor supports double-click based application of edits from one lines to the other.

func NewDiffTextEditor

func NewDiffTextEditor(parent ...tree.Node) *DiffTextEditor

NewDiffTextEditor returns a new DiffTextEditor with the given optional parent: DiffTextEditor supports double-click based application of edits from one lines to the other.

func (*DiffTextEditor) Init

func (ed *DiffTextEditor) Init()

type Editor

type Editor struct {
	Base

	// ISearch is the interactive search data.
	ISearch ISearch `set:"-" edit:"-" json:"-" xml:"-"`

	// QReplace is the query replace data.
	QReplace QReplace `set:"-" edit:"-" json:"-" xml:"-"`

	// Complete is the functions and data for text completion.
	Complete *core.Complete `json:"-" xml:"-"`
	// contains filtered or unexported fields
}

Editor is a widget for editing multiple lines of complicated text (as compared to core.TextField for a single line of simple text). The Editor is driven by a lines.Lines buffer which contains all the text, and manages all the edits, sending update events out to the editors.

Use NeedsRender to drive an render update for any change that does not change the line-level layout of the text.

Multiple editors can be attached to a given buffer. All updating in the Editor should be within a single goroutine, as it would require extensive protections throughout code otherwise.

func AsEditor

func AsEditor(n tree.Node) *Editor

AsEditor returns the given value as a value of type Editor if the type of the given value embeds Editor, or nil otherwise

func NewEditor

func NewEditor(parent ...tree.Node) *Editor

NewEditor returns a new Editor with the given optional parent: Editor is a widget for editing multiple lines of complicated text (as compared to core.TextField for a single line of simple text). The Editor is driven by a lines.Lines buffer which contains all the text, and manages all the edits, sending update events out to the editors.

Use NeedsRender to drive an render update for any change that does not change the line-level layout of the text.

Multiple editors can be attached to a given buffer. All updating in the Editor should be within a single goroutine, as it would require extensive protections throughout code otherwise.

func TextDialog

func TextDialog(ctx core.Widget, title, text string) *Editor

TextDialog opens a dialog for displaying text string

func (*Editor) AsEditor

func (t *Editor) AsEditor() *Editor

AsEditor satisfies the EditorEmbedder interface

func (*Editor) CancelComplete

func (ed *Editor) CancelComplete()

CancelComplete cancels any pending completion. Call this when new events have moved beyond any prior completion scenario.

func (*Editor) Close

func (ed *Editor) Close(afterFunc func(canceled bool)) bool

Close closes the lines viewed by this editor, prompting to save if there are changes. If afterFunc is non-nil, then it is called with the status of the user action.

func (*Editor) Init

func (ed *Editor) Init()

func (*Editor) JumpToLinePrompt

func (ed *Editor) JumpToLinePrompt()

JumpToLinePrompt jumps to given line number (minus 1) from prompt

func (*Editor) Lookup

func (ed *Editor) Lookup()

Lookup attempts to lookup symbol at current location, popping up a window if something is found.

func (*Editor) QReplacePrompt

func (ed *Editor) QReplacePrompt()

QReplacePrompt is an emacs-style query-replace mode -- this starts the process, prompting user for items to search etc

func (*Editor) QReplaceReplaceAll

func (ed *Editor) QReplaceReplaceAll(midx int)

QReplaceReplaceAll replaces all remaining from index

func (*Editor) QReplaceStart

func (ed *Editor) QReplaceStart(find, repl string, lexItems bool)

QReplaceStart starts query-replace using given find, replace strings

func (*Editor) Save

func (ed *Editor) Save() error

Save saves the current text into the current filename associated with this buffer. Do NOT use this in an OnChange event handler as it emits a Change event! Use Editor.SaveQuiet instead.

func (*Editor) SaveAs

func (ed *Editor) SaveAs(filename core.Filename)

SaveAs saves the current text into given file; does an editDone first to save edits and checks for an existing file; if it does exist then prompts to overwrite or not.

func (*Editor) SaveQuiet

func (ed *Editor) SaveQuiet() error

SaveQuiet saves the current text into the current filename associated with this buffer. This version does not emit a change event, so it is safe to use in an OnChange event handler, unlike Editor.Save.

func (*Editor) SetComplete

func (t *Editor) SetComplete(v *core.Complete) *Editor

SetComplete sets the [Editor.Complete]: Complete is the functions and data for text completion.

func (*Editor) SetLines

func (ed *Editor) SetLines(ln *lines.Lines) *Editor

SetLines sets the lines.Lines that this is an editor of, creating a new view for this editor and connecting to events.

func (*Editor) ShowContextMenu

func (ed *Editor) ShowContextMenu(e events.Event)

ShowContextMenu displays the context menu with options dependent on situation

func (*Editor) SpellCheckLineErrors

func (ed *Editor) SpellCheckLineErrors(ln int) lexer.Line

SpellCheckLineErrors runs spell check on given line, and returns Lex tags with token.TextSpellErr for any misspelled words

func (*Editor) UpdateNewFile

func (ed *Editor) UpdateNewFile()

UpdateNewFile checks if there is a new file in the Lines editor and updates any relevant editor settings accordingly.

type EditorEmbedder

type EditorEmbedder interface {
	AsEditor() *Editor
}

EditorEmbedder is an interface that all types that embed Editor satisfy

type ISearch

type ISearch struct {

	// if true, in interactive search mode
	On bool `json:"-" xml:"-"`

	// current interactive search string
	Find string `json:"-" xml:"-"`

	// current search matches
	Matches []textpos.Match `json:"-" xml:"-"`
	// contains filtered or unexported fields
}

ISearch holds all the interactive search data

type OutputBuffer

type OutputBuffer struct {

	// the output that we are reading from, as an io.Reader
	Output io.Reader

	// the [lines.Lines] that we output to
	Lines *lines.Lines

	// how much time to wait while batching output (default: 200ms)
	Batch time.Duration

	// MarkupFunc is an optional markup function that adds html tags to given line
	// of output. It is essential that it not add any new text, just splits into spans
	// with different styles.
	MarkupFunc OutputBufferMarkupFunc

	// mutex protecting updates
	sync.Mutex
	// contains filtered or unexported fields
}

OutputBuffer is a buffer that records the output from an io.Reader using bufio.Scanner. It is optimized to combine fast chunks of output into large blocks of updating. It also supports an arbitrary markup function that operates on each line of output text.

func (*OutputBuffer) MonitorOutput

func (ob *OutputBuffer) MonitorOutput()

MonitorOutput monitors the output and updates the [Buffer].

func (*OutputBuffer) SetBatch

func (t *OutputBuffer) SetBatch(v time.Duration) *OutputBuffer

SetBatch sets the [OutputBuffer.Batch]: how much time to wait while batching output (default: 200ms)

func (*OutputBuffer) SetLines

func (t *OutputBuffer) SetLines(v *lines.Lines) *OutputBuffer

SetLines sets the [OutputBuffer.Lines]: the lines.Lines that we output to

func (*OutputBuffer) SetMarkupFunc

func (t *OutputBuffer) SetMarkupFunc(v OutputBufferMarkupFunc) *OutputBuffer

SetMarkupFunc sets the [OutputBuffer.MarkupFunc]: MarkupFunc is an optional markup function that adds html tags to given line of output. It is essential that it not add any new text, just splits into spans with different styles.

func (*OutputBuffer) SetOutput

func (t *OutputBuffer) SetOutput(v io.Reader) *OutputBuffer

SetOutput sets the [OutputBuffer.Output]: the output that we are reading from, as an io.Reader

type OutputBufferMarkupFunc

type OutputBufferMarkupFunc func(buf *lines.Lines, line []rune) rich.Text

OutputBufferMarkupFunc is a function that returns a marked-up version of a given line of output text. It is essential that it not add any new text, just splits into spans with different styles.

type QReplace

type QReplace struct {

	// if true, in interactive search mode
	On bool `json:"-" xml:"-"`

	// current interactive search string
	Find string `json:"-" xml:"-"`

	// current interactive search string
	Replace string `json:"-" xml:"-"`

	// current search matches
	Matches []textpos.Match `json:"-" xml:"-"`
	// contains filtered or unexported fields
}

QReplace holds all the query-replace data

type TwinEditors

type TwinEditors struct {
	core.Splits

	// [Buffer] for A
	BufferA *lines.Lines `json:"-" xml:"-"`

	// [Buffer] for B
	BufferB *lines.Lines `json:"-" xml:"-"`
	// contains filtered or unexported fields
}

TwinEditors presents two side-by-side [Editor]s in core.Splits that scroll in sync with each other.

func NewTwinEditors

func NewTwinEditors(parent ...tree.Node) *TwinEditors

NewTwinEditors returns a new TwinEditors with the given optional parent: TwinEditors presents two side-by-side [Editor]s in core.Splits that scroll in sync with each other.

func (*TwinEditors) Editors

func (te *TwinEditors) Editors() (*Editor, *Editor)

Editors returns the two text [Editor]s.

func (*TwinEditors) Init

func (te *TwinEditors) Init()

func (*TwinEditors) SetBufferA

func (t *TwinEditors) SetBufferA(v *lines.Lines) *TwinEditors

SetBufferA sets the [TwinEditors.BufferA]: [Buffer] for A

func (*TwinEditors) SetBufferB

func (t *TwinEditors) SetBufferB(v *lines.Lines) *TwinEditors

SetBufferB sets the [TwinEditors.BufferB]: [Buffer] for B

func (*TwinEditors) SetFiles

func (te *TwinEditors) SetFiles(fileA, fileB string)

SetFiles sets the files for each [Buffer].

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL