Verified Commit ece29ce3 authored by Camil Staps's avatar Camil Staps 🚀

Add basic file links (#3; no icl linking yet); when --include-applications is...

Add basic file links (#3; no icl linking yet); when --include-applications is used, still index the dcl if it exists
parent 0f4bcff8
Pipeline #14675 failed with stage
in 55 seconds
......@@ -12,6 +12,28 @@ a.hidden {
float: right;
}
table td {
vertical-align: top;
}
.linenos {
background: #ddd;
color: #666;
margin-right: .5em;
text-align: right;
}
.linenos > span {
box-sizing: border-box;
padding: 0 .5em 0 1em;
width: 100%;
}
.linenos .active {
background: #666;
color: #ddd;
}
/* Main boxes */
#container {
position: relative;
......@@ -187,12 +209,12 @@ a.hidden {
content: '\2303';
}
.collapsable-link {
.collapsable-head a {
color: #666;
margin: 0 .5em;
text-decoration: none;
}
.collapsable-link:hover {
.collapsable-head a:hover {
color: #111;
}
......
......@@ -67,9 +67,9 @@ function highlightCallback(span, cls, str) {
elem.classList.toggle('uncollapsed');
};
let link = head.querySelector('.collapsable-link');
if (link != null)
link.onclick = function (e) { e.stopPropagation(); };
let links = head.querySelectorAll('.collapsable-link, .file-link');
for (let i = 0; i < links.length; i++)
links[i].onclick = function (e) { e.stopPropagation(); };
}
/* Open collapsable of hashtag in url */
......@@ -77,9 +77,14 @@ function highlightCallback(span, cls, str) {
if (window.location.hash.substring(0,1) == '#') {
let id = decodeURIComponent(window.location.hash.substring(1));
let elem = document.getElementById(id);
if (elem == null)
return;
if (elem.classList.contains('collapsable')) {
elem.collapsableOpen();
}
if (elem.parentNode.classList.contains('linenos')) {
elem.classList.add('active');
}
}
};
window.onhashchange();
......@@ -100,63 +105,71 @@ function highlightCallback(span, cls, str) {
/* Search field */
let searchfield = document.getElementById('search-field');
let searchresults = document.getElementById('search-results').querySelector('ul');
let searchcloogle = document.getElementById('search-cloogle');
searchfield.oninput = function () {
searchresults.innerHTML = '';
searchcloogle.innerHTML = '';
if (searchfield != null) {
let searchresults = document.getElementById('search-results').querySelector('ul');
let searchcloogle = document.getElementById('search-cloogle');
searchfield.oninput = function () {
searchresults.innerHTML = '';
searchcloogle.innerHTML = '';
sessionStorage.searchValue = this.value;
sessionStorage.searchValue = this.value;
if (this.value == '')
return;
if (this.value == '')
return;
var results = lunr_index.search(this.value);
var results = lunr_index.search(this.value);
var cloogle = '<a href="https://cloogle.org/#' + this.value + '" target="_blank">search on Cloogle</a>';
var cloogle = '<a href="https://cloogle.org/#' + this.value + '" target="_blank">search on Cloogle</a>';
if (results.length == 0) {
searchresults.innerHTML = '<em>no results</em> &mdash; ' + cloogle;
return;
}
if (results.length == 0) {
searchresults.innerHTML = '<em>no results</em> &mdash; ' + cloogle;
return;
}
searchcloogle.innerHTML = cloogle.replace('search on Cloogle', 'Find more on Cloogle');
for (let i in results) {
let elem = lunr_items[results[i].ref];
let li = document.createElement('li');
let icon = document.createElement('span');
icon.classList = 'icon icon-' + elem.kind;
li.appendChild(icon);
let a = document.createElement('a');
a.href = make_relative_url('mod/' + elem.module + '.html#' + elem.htmlid);
if (elem.kind == 'module')
a.innerHTML = '<code>' + elem.name + '</code>';
else
a.innerHTML = '<code>' + elem.name + '</code> in <code>' + elem.module + '</code>';
li.appendChild(a);
searchresults.appendChild(li);
}
};
searchcloogle.innerHTML = cloogle.replace('search on Cloogle', 'Find more on Cloogle');
for (let i in results) {
let elem = lunr_items[results[i].ref];
let li = document.createElement('li');
let icon = document.createElement('span');
icon.classList = 'icon icon-' + elem.kind;
li.appendChild(icon);
let a = document.createElement('a');
a.href = make_relative_url('mod/' + elem.module + '.html#' + elem.htmlid);
if (elem.kind == 'module')
a.innerHTML = '<code>' + elem.name + '</code>';
else
a.innerHTML = '<code>' + elem.name + '</code> in <code>' + elem.module + '</code>';
li.appendChild(a);
searchresults.appendChild(li);
}
};
}
/* Resizable sidebar */
let sidebar = document.getElementById('sidebar');
sidebar.onmousemove = function () {
document.getElementById('content').style.left = this.offsetWidth + 'px';
};
sidebar.onmouseup = function () { /* not every browser supports onresize on div */
this.onmousemove();
sessionStorage['sidebar-width'] = this.offsetWidth;
};
if (sidebar != null) {
sidebar.onmousemove = function () {
document.getElementById('content').style.left = this.offsetWidth + 'px';
};
sidebar.onmouseup = function () { /* not every browser supports onresize on div */
this.onmousemove();
sessionStorage['sidebar-width'] = this.offsetWidth;
};
}
let toc = document.getElementById('toc');
toc.onmouseup = function () {
sessionStorage['toc-height'] = this.offsetHeight;
};
if (toc != null) {
toc.onmouseup = function () {
sessionStorage['toc-height'] = this.offsetHeight;
};
}
let savedScrollDivs = ['toc', 'search'];
for (let i in savedScrollDivs) {
let elem = document.getElementById(savedScrollDivs[i]);
if (elem == null)
continue;
elem.onscroll = function() {
sessionStorage['scrollTop-' + savedScrollDivs[i]] = this.scrollTop;
sessionStorage['scrollLeft-' + savedScrollDivs[i]] = this.scrollLeft;
......@@ -171,24 +184,29 @@ function highlightCallback(span, cls, str) {
for (let i in savedScrollDivs) {
let elem = document.getElementById(savedScrollDivs[i]);
if (elem == null)
continue;
if (typeof sessionStorage['scrollTop-' + savedScrollDivs[i]] != 'undefined')
elem.scrollTop = sessionStorage['scrollTop-' + savedScrollDivs[i]];
if (typeof sessionStorage['scrollLeft-' + savedScrollDivs[i]] != 'undefined')
elem.scrollLeft = sessionStorage['scrollLeft-' + savedScrollDivs[i]];
}
if (typeof sessionStorage['scrollTop-toc'] == 'undefined') {
let toc = document.getElementById('toc');
let active = toc.querySelector('.active');
if (active != null)
toc.scrollTop = active.offsetTop - 50;
if (sidebar != null) {
if (typeof sessionStorage['sidebar-width'] != 'undefined')
sidebar.style.width = sessionStorage['sidebar-width'] + 'px';
sidebar.onmousemove();
}
if (typeof sessionStorage['sidebar-width'] != 'undefined')
sidebar.style.width = sessionStorage['sidebar-width'] + 'px';
sidebar.onmousemove();
if (toc != null) {
if (typeof sessionStorage['scrollTop-toc'] == 'undefined') {
let active = toc.querySelector('.active');
if (active != null)
toc.scrollTop = active.offsetTop - 50;
}
if (typeof sessionStorage['toc-height'] != 'undefined')
toc.style.height = sessionStorage['toc-height'] + 'px';
if (typeof sessionStorage['toc-height'] != 'undefined')
toc.style.height = sessionStorage['toc-height'] + 'px';
}
};
})();
......@@ -13,6 +13,7 @@ from System.FilePath import :: FilePath
:: Described element doc =
{ name :: !String
, pos :: !ElementPosition
, repr :: !Maybe String
, elem :: !element
, doc :: !Maybe doc
......@@ -20,9 +21,17 @@ from System.FilePath import :: FilePath
instance < (Described e d)
:: ElementPosition =
{ dcl_file :: !FilePath
, dcl_line :: !Maybe Int
, icl_line :: !Maybe Int
}
:: DescribedModule :== Described ModuleDescription ModuleDoc
:: ModuleDescription =
{ mod_type_defs :: ![DescribedTypeDef]
{ mod_dcl :: !Maybe String
, mod_icl :: !String
, mod_type_defs :: ![DescribedTypeDef]
, mod_classes :: ![DescribedClass]
, mod_generics :: ![DescribedGeneric]
, mod_functions :: ![DescribedFunction]
......
implementation module Clean.Doc.ModuleCollection
import StdArray
import StdChar
import StdFile
import StdFunctions
import StdInt
import StdMisc
import StdString
......@@ -17,33 +20,53 @@ import Data.Error
from Data.Func import instance Functor ((->) a)
import Data.Functor
import Data.Maybe
import Data.Tuple
import System.File
from syntax import
:: ClassDef{class_ident},
:: ClassDef{class_ident,class_pos},
:: FileName,
:: FunctName,
:: FunKind(FK_Macro),
:: FunSpecials,
:: GenericDef{gen_ident},
:: GenericCaseDef{gc_pos},
:: GenericDef{gen_ident,gen_pos},
:: Ident{id_name},
:: Import{import_file_position},
:: ImportedObject,
:: LineNr,
:: Module{mod_defs,mod_ident},
:: Optional(Yes),
:: ParsedDefinition(PD_Class,PD_Function,PD_Generic,PD_Type,PD_TypeSpec),
:: ParsedDefinition(PD_Class,PD_Derive,PD_Erroneous,PD_ForeignExport,
PD_Function,PD_Generic,PD_GenericCase,PD_Import,PD_ImportedObjects,
PD_Instance,PD_Instances,PD_NodeDef,PD_Type,PD_TypeSpec),
:: ParsedExpr,
:: ParsedImport,
:: ParsedInstance{pi_pos},
:: ParsedInstanceAndMembers{pim_pi},
:: ParsedTypeDef,
:: Position(FunPos,LinePos),
:: Priority,
:: Rhs,
:: RhsDefsOfType,
:: SymbolType,
:: TypeDef{td_ident}
:: TypeDef{td_ident,td_pos}
instance < (Described e d) where < a b = a.Described.name < b.Described.name
collectModule :: !ModuleFindingOptions !FilePath !*World
-> *(!MaybeError String DescribedModule, !*World)
collectModule mfo fp w
collectModule mfo fp w`
# fp = if dclexi (fp:=(size fp-3,'d')) fp
# (comments,w) = scanComments fp w
# (dcl,w) = readModule fp w
| isError dcl = (Error (fromError dcl), w)
# (dcl,_) = fromOk dcl
# (Ok iclcontents,w) = readFile (fp:=(size fp-3,'i')) w
# (dclcontents,w) = appFst error2mb (readFile (fp:=(size fp-3,'d')) w)
# (mod,dcldefs,doc) = (dcl, dcl.mod_defs, case comments of
Error _ -> emptyCollectedComments
Ok comments -> collectComments comments dcl)
......@@ -52,9 +75,12 @@ collectModule mfo fp w
_ -> Nothing
# moddesc =
{ name = mod.mod_ident.id_name
, pos = {position Nothing Nothing & dcl_line=Just 1, icl_line=Just 1}
, repr = Nothing
, elem =
{ mod_type_defs = [collectTypeDef doc pd \\ pd=:(PD_Type _) <- dcldefs]
{ mod_dcl = dclcontents
, mod_icl = iclcontents
, mod_type_defs = [collectTypeDef doc pd \\ pd=:(PD_Type _) <- dcldefs]
, mod_classes = [collectClass doc pd \\ pd=:(PD_Class _ _) <- dcldefs]
, mod_generics = [collectGeneric doc pd \\ pd=:(PD_Generic _) <- dcldefs]
, mod_functions = [collectFunction doc pd \\ pd=:(PD_TypeSpec _ _ _ (Yes _) _) <- dcldefs]
......@@ -64,9 +90,14 @@ collectModule mfo fp w
}
= (Ok moddesc, w)
where
(dclexi,w) = fileExists (fp:=(size fp-3,'d')) w`
isdcl = fp.[size fp-3] == 'd'
dclfirst = dclexi
collectTypeDef :: !CollectedComments !ParsedDefinition -> DescribedTypeDef
collectTypeDef cc pd=:(PD_Type ptd) =
{ name = ptd.td_ident.id_name
, pos = position (Just pd) Nothing
, repr = Just (cpp pd)
, elem = TypeDefDescription
, doc = case parseDoc <$> getComment pd cc of
......@@ -77,6 +108,7 @@ where
collectClass :: !CollectedComments !ParsedDefinition -> DescribedClass
collectClass cc pd=:(PD_Class cd members) =
{ name = cd.class_ident.id_name
, pos = position (Just pd) Nothing
, repr = Just (cpp pd)
, elem =
{ class_members = [collectFunction cc m \\ m <- members]
......@@ -89,6 +121,7 @@ where
collectGeneric :: !CollectedComments !ParsedDefinition -> DescribedGeneric
collectGeneric cc pd=:(PD_Generic gd) =
{ name = gd.gen_ident.id_name
, pos = position (Just pd) Nothing
, repr = Just (cpp pd)
, elem = GenericDescription
, doc = case parseDoc <$> getComment pd cc of
......@@ -97,8 +130,9 @@ where
}
collectFunction :: !CollectedComments !ParsedDefinition -> DescribedFunction
collectFunction cc pd=:(PD_TypeSpec _ id _ _ _) =
collectFunction cc pd=:(PD_TypeSpec pos id _ _ _) =
{ name = id.id_name
, pos = position (Just pd) Nothing
, repr = Just (cpp pd)
, elem = FunctionDescription
, doc = case parseDoc <$> getComment pd cc of
......@@ -107,8 +141,9 @@ where
}
collectMacro :: ![ParsedDefinition] !CollectedComments !ParsedDefinition -> DescribedMacro
collectMacro allpds cc pd=:(PD_Function _ id _ _ _ _) =
collectMacro allpds cc pd=:(PD_Function pos id _ _ _ _) =
{ name = id.id_name
, pos = position (Just pd) Nothing
, repr = Just (fromMaybe "" ((flip (+++) "\n") <$> cpp <$> typespec) +++ cpp pd)
, elem = MacroDescription
, doc = case parseDoc <$> comment of
......@@ -124,3 +159,33 @@ where
findTypeSpec id [pd=:(PD_TypeSpec _ id` prio _ _):defs]
| id`.id_name == id.id_name = Just pd
findTypeSpec id [_:defs] = findTypeSpec id defs
position :: !(Maybe ParsedDefinition) !(Maybe ParsedDefinition) -> ElementPosition
position dcl icl =
{ dcl_file = fp:=(size fp-3, 'd')
, dcl_line = pdLine =<< if dclexi dcl icl
, icl_line = pdLine =<< if dclexi icl dcl
}
where
pdLine :: !ParsedDefinition -> Maybe Int
pdLine pd = case pd of
PD_Function pos _ _ _ _ _ -> positionLine pos
PD_NodeDef pos _ _ -> positionLine pos
PD_Type ptd -> positionLine ptd.td_pos
PD_TypeSpec pos _ _ _ _ -> positionLine pos
PD_Class cd _ -> positionLine cd.class_pos
PD_Instance piam -> positionLine piam.pim_pi.pi_pos
PD_Instances [piam:_] -> positionLine piam.pim_pi.pi_pos
PD_Import [pi:_] -> positionLine pi.import_file_position
PD_ImportedObjects _ -> Nothing
PD_ForeignExport _ _ _ _ -> Nothing
PD_Generic gd -> positionLine gd.gen_pos
PD_GenericCase gcd _ -> positionLine gcd.gc_pos
PD_Derive [gcd:_] -> positionLine gcd.gc_pos
PD_Erroneous -> Nothing
where
positionLine :: Position -> Maybe Int
positionLine p = case p of
FunPos _ l _ -> Just l
LinePos _ l -> Just l
_ -> Nothing
......@@ -30,13 +30,18 @@ import Clean.Doc.ModuleCollection.Index => qualified :: IndexItem{name}
generateHTML :: !ModuleCollection -> WebSite
generateHTML mcoll =
[("js/lunr-index.js", "build_lunr(" +++ (toString $ toJSON $ indexModuleCollection mcoll) +++ ");")] ++
map (\(fp,html) -> (fp,finalizeHTML fp html))
map (\(fp,html) -> (fp,finalizeHtml fp $ addSideBar fp html))
[ ("index.html", Text "")
: map moduleFile mcoll
]
] ++
map (\(fp,html) -> (fp,finalizeHtml fp html)) (flatten
[catMaybes
[ highlightedFile dcl_file <$> elem.mod_dcl
, Just $ highlightedFile (dcl_file:=(size dcl_file-3,'i')) elem.mod_icl
] \\ {pos={dcl_file},elem} <- mcoll])
where
finalizeHTML :: FilePath HtmlTag -> String
finalizeHTML fp html = (+++) "<!DOCTYPE html>" $ toString $ HtmlTag [LangAttr "en"]
finalizeHtml :: !FilePath !HtmlTag -> String
finalizeHtml fp html = (+++) "<!DOCTYPE html>" $ toString $ HtmlTag [LangAttr "en"]
[ HeadTag []
[ TitleTag [] [Text "Documentation"]
, MetaTag [CharsetAttr "utf-8"]
......@@ -49,27 +54,25 @@ where
, ScriptTag [DeferAttr "defer", SrcAttr (relativePath "modules/lunr.js/lunr.js")] []
, ScriptTag [DeferAttr "defer", SrcAttr (relativePath "js/lunr-index.js")] []
]
, BodyTag []
[ DivTag [IdAttr "container"]
[ DivTag [IdAttr "content"] [html]
, DivTag [IdAttr "sidebar"]
[ DivTag [IdAttr "toc"] [H1Tag [] [Text "Documentation"], UlTag [] (map sidebarLink (sort mcoll))]
, DivTag [IdAttr "search"]
[ InputTag [IdAttr "search-field", TypeAttr "text", PlaceholderAttr "search"]
, DivTag [IdAttr "search-results"] [UlTag [] []]
, PTag [IdAttr "search-cloogle"] []
]
]
, BodyTag [] [html]
]
where
relativePath p = p relativeTo fp
addSideBar :: FilePath HtmlTag -> HtmlTag
addSideBar fp html = DivTag [IdAttr "container"]
[ DivTag [IdAttr "content"] [html]
, DivTag [IdAttr "sidebar"]
[ DivTag [IdAttr "toc"] [H1Tag [] [Text "Documentation"], UlTag [] (map sidebarLink (sort mcoll))]
, DivTag [IdAttr "search"]
[ InputTag [IdAttr "search-field", TypeAttr "text", PlaceholderAttr "search"]
, DivTag [IdAttr "search-results"] [UlTag [] []]
, PTag [IdAttr "search-cloogle"] []
]
]
]
where
relativePath :: FilePath -> FilePath
relativePath to = concat (repeatn (dirLength fp-1) {'.','.',pathSeparator}) +++ to
dirLength :: FilePath -> Int
dirLength "" = 0
dirLength fp = 1 + dirLength (takeDirectory fp)
relativePath p = p relativeTo fp
sidebarLink :: !DescribedModule -> HtmlTag
sidebarLink dm = LiTag []
......@@ -82,8 +85,10 @@ where
where thispath = "mod" </> dm.name +++ ".html"
moduleFile :: !DescribedModule -> (FilePath, HtmlTag)
moduleFile m = ("mod" </> m.name +++ ".html", html)
moduleFile m = (thisfp, html)
where
thisfp = "mod" </> m.name +++ ".html"
html = DivTag [] $ catMaybes
[ Just $ H1Tag [IdAttr ("mod-" +++ m.name)] [Text m.name]
, DivTag [ClassAttr "clean-doc"] <$> pure <$> Text <$> (docDescription =<< m.doc)
......@@ -99,7 +104,7 @@ where
: map html (sort m.elem.mod_type_defs)
]
where
html dtd = collapsable "typedef" ("td-" +++ dtd.name) dtd.name $ catMaybes
html dtd = collapsable thisfp "typedef" ("td-" +++ dtd.name) dtd.name dtd.pos $ catMaybes
[ DivTag [ClassAttr "clean-doc"] <$> pure <$> Text <$> (docDescription =<< dtd.doc)
, Just $ PreTag [ClassAttr "clean-code"] [Html (fromJust dtd.repr)]
]
......@@ -109,7 +114,7 @@ where
: map html (sort m.elem.mod_classes)
]
where
html dc = collapsable "class" ("class-" +++ dc.name) dc.name $ catMaybes
html dc = collapsable thisfp "class" ("class-" +++ dc.name) dc.name dc.pos $ catMaybes
[ DivTag [ClassAttr "clean-doc"] <$> pure <$> Text <$> (docDescription =<< dc.doc)
, Just $ PreTag [ClassAttr "clean-code"] [Html (fromJust dc.repr)]
]
......@@ -119,7 +124,7 @@ where
: map html (sort m.elem.mod_generics)
]
where
html dg = collapsable "generic" ("gen-" +++ dg.name) dg.name $ catMaybes
html dg = collapsable thisfp "generic" ("gen-" +++ dg.name) dg.name dg.pos $ catMaybes
[ DivTag [ClassAttr "clean-doc"] <$> pure <$> Text <$> (docDescription =<< dg.doc)
, Just $ PreTag [ClassAttr "clean-code"] [Html (fromJust dg.repr)]
]
......@@ -129,7 +134,7 @@ where
: map html (sort m.elem.mod_functions)
]
where
html df = collapsable "function" ("fun-" +++ df.name) df.name $ catMaybes
html df = collapsable thisfp "function" ("fun-" +++ df.name) df.name df.pos $ catMaybes
[ DivTag [ClassAttr "clean-doc"] <$> pure <$> Text <$> (docDescription =<< df.doc)
, Just $ PreTag [ClassAttr "clean-code"] [Html (fromJust df.repr)]
]
......@@ -139,24 +144,62 @@ where
: map html (sort m.elem.mod_macros)
]
where
html df = collapsable "macro" ("macro-" +++ df.name) df.name $ catMaybes
[ DivTag [ClassAttr "clean-doc"] <$> pure <$> Text <$> (docDescription =<< df.doc)
, Just $ PreTag [ClassAttr "clean-code"] [Html (fromJust df.repr)]
html dm = collapsable thisfp "macro" ("macro-" +++ dm.name) dm.name dm.pos $ catMaybes
[ DivTag [ClassAttr "clean-doc"] <$> pure <$> Text <$> (docDescription =<< dm.doc)
, Just $ PreTag [ClassAttr "clean-code"] [Html (fromJust dm.repr)]
]
collapsable :: !String !String !String !a -> HtmlTag | html a
collapsable icon id title body = DivTag
collapsable :: !FilePath !String !String !String !ElementPosition !a -> HtmlTag | html a
collapsable thisfp icon id title pos body = DivTag
[ ClassAttr "collapsable"
, IdAttr id
]
[ DivTag [ClassAttr ("collapsable-head collapsable-head-" +++ icon)]
[ SpanTag [ClassAttr ("icon icon-" +++ icon)] []
, Text title
, SpanTag [ClassAttr "flush-right collapsable-indicator"] []
, ATag [ClassAttr "flush-right collapsable-link", HrefAttr ("#" +++ id), TitleAttr "URL to this item"] [Text "#"]
[ DivTag [ClassAttr ("collapsable-head collapsable-head-" +++ icon)] $ catMaybes
[ Just $ SpanTag [ClassAttr ("icon icon-" +++ icon)] []
, Just $ Text title
, Just $ SpanTag [ClassAttr "flush-right collapsable-indicator"] []
, Just $ ATag [ClassAttr "flush-right collapsable-link", HrefAttr ("#" +++ id), TitleAttr "URL to this item"] [Text "#"]
, file_link True <$> pos.dcl_line
, file_link False <$> pos.icl_line
]
, DivTag [ClassAttr "collapsable-body"] [html body]
]
where
file_link :: !Bool !Int -> HtmlTag
file_link dcl line = ATag
[ ClassAttr "flush-right file-link"
, TargetAttr "_blank"
, HrefAttr $
("file" </?> (if dcl pos.dcl_file (pos.dcl_file:=(size pos.dcl_file-3,'i'))) +++ ".html#l" <+ line)
relativeTo thisfp
]
[Text (if dcl "dcl:" "icl:" <+ line)]
highlightedFile :: !FilePath !String -> (FilePath, HtmlTag)
highlightedFile fp contents = ("file" </?> fp +++ ".html", TableTag [] [TrTag []
[ TdTag [] [PreTag [ClassAttr "linenos"] [SpanTag [IdAttr ("l" <+ i)] [Text (i <+ "\n")] \\ i <- [1..lineCount contents]]]
, TdTag [] [PreTag [ClassAttr "clean-code"] [Text contents]]
]])
where
lineCount :: !String -> Int
lineCount s = count 1 (size s-1)
where