1- from collections .abc import (
2- Iterator ,
3- Generator ,
4- )
1+ from collections .abc import Generator
52import contextlib
63import dataclasses
74import datetime
2623from trove .util .iris import get_sufficiently_unique_iri
2724from trove .util .randomness import shuffled
2825from trove .vocab import mediatypes
29- from trove .vocab .namespaces import RDF , RDFS , SKOS , DCTERMS , FOAF , DC
26+ from trove .vocab import jsonapi
27+ from trove .vocab .namespaces import RDF , RDFS , SKOS , DCTERMS , FOAF , DC , OSFMAP
3028from trove .vocab .static_vocab import combined_thesaurus__suffuniq
3129from trove .vocab .trove import trove_browse_link
3230from ._base import BaseRenderer
4947 DCTERMS .title ,
5048 DC .title ,
5149 FOAF .name ,
50+ OSFMAP .fileName ,
5251)
5352_IMPLICIT_DATATYPES = frozenset ((
5453 RDF .string ,
5554 RDF .langString ,
5655))
56+ _PREDICATES_RENDERED_SPECIAL = frozenset ((
57+ RDF .type ,
58+ ))
59+ _PRIMITIVE_LITERAL_TYPES = (float , int , datetime .date )
5760
5861_QUERYPARAM_SPLIT_RE = re .compile (r'(?=[?&])' )
5962
@@ -81,11 +84,13 @@ def is_data_blended(self) -> bool | None:
8184 def simple_render_document (self ) -> str :
8285 self .__hb = HtmlBuilder ()
8386 self .render_html_head ()
84- _body_attrs = {
85- 'class' : 'BrowseWrapper' ,
86- 'style' : self ._hue_turn_css (),
87- }
88- with self .__hb .nest ('body' , attrs = _body_attrs ):
87+ with (
88+ self ._hue_turn_css () as _hue_turn_style ,
89+ self .__hb .nest ('body' , attrs = {
90+ 'class' : 'BrowseWrapper' ,
91+ 'style' : _hue_turn_style ,
92+ }),
93+ ):
8994 self .render_nav ()
9095 self .render_main ()
9196 self .render_footer ()
@@ -147,51 +152,64 @@ def __mediatype_link(self, mediatype: str) -> None:
147152 with self .__hb .nest ('a' , attrs = {'href' : reverse ('trove:docs' )}) as _link :
148153 _link .text = _ ('(stable for documented use)' )
149154
150- def __render_subj (self , subj_iri : str , * , start_collapsed : bool | None = None ) -> None :
155+ def __render_subj (self , subj_iri : str , * , should_link_details : bool = False ) -> None :
151156 _twopledict = self .__current_data .get (subj_iri , {})
152- with self .__visiting (subj_iri ):
157+ _is_focus = (subj_iri in self .response_focus .iris )
158+ with self .__visiting (subj_iri ) as _h_tag :
153159 with self .__nest_card ('article' ):
154160 with self .__hb .nest ('header' ):
155161 _compact = self .iri_shorthand .compact_iri (subj_iri )
156162 _is_compactable = (_compact != subj_iri )
157- _should_link = (subj_iri not in self .response_focus .iris )
158- with self .__hb .nest_h_tag (attrs = {'id' : quote (subj_iri )}) as _h :
159- if _should_link :
163+ with self .__hb .nest (_h_tag , attrs = {'id' : quote (subj_iri )}) as _h :
164+ if _is_focus :
165+ if _is_compactable :
166+ _h .text = _compact
167+ else :
168+ self .__split_iri_pre (subj_iri )
169+ else :
160170 with self .__nest_link (subj_iri ) as _link :
161171 if _is_compactable :
162172 _link .text = _compact
163173 else :
164174 self .__split_iri_pre (subj_iri )
165- else :
166- if _is_compactable :
167- _h .text = _compact
168- else :
169- self .__split_iri_pre (subj_iri )
170175 self .__iri_subheaders (subj_iri )
171- if _twopledict :
172- with self .__hb .nest ('details' ) as _details :
173- _detail_depth = sum ((_el .tag == 'details' ) for _el in self .__hb ._nested_elements )
174- _should_open = (
175- _detail_depth < 3
176- if start_collapsed is None
177- else not start_collapsed
178- )
179- if _should_open :
180- _details .set ('open' , '' )
176+ if _is_compactable :
177+ self .__hb .leaf ('pre' , text = subj_iri )
178+ if should_link_details :
179+ with self .__nest_link (subj_iri ) as _link :
180+ _link .text = _ ('more details...' )
181+ elif _twopledict :
182+ _details_attrs = (
183+ {'open' : '' }
184+ if (_is_focus or _is_local_url (subj_iri ))
185+ else {}
186+ )
187+ with self .__hb .nest ('details' , _details_attrs ):
181188 self .__hb .leaf ('summary' , text = _ ('more details...' ))
182189 self .__twoples (_twopledict )
183190
184191 def __twoples (self , twopledict : rdf .RdfTwopleDictionary ) -> None :
185192 with self .__hb .nest ('dl' , {'class' : 'Browse__twopleset' }):
186- for _pred , _obj_set in shuffled ( twopledict . items () ):
193+ for _pred , _obj_set in self . __order_twopledict ( twopledict ):
187194 with self .__hb .nest ('dt' , attrs = {'class' : 'Browse__predicate' }):
188195 self .__compact_link (_pred )
189196 for _text in self .__iri_thesaurus_labels (_pred ):
190197 self .__literal (_text )
191198 with self .__hb .nest ('dd' ):
192- for _obj in shuffled ( _obj_set ) :
199+ for _obj in _obj_set :
193200 self .__obj (_obj )
194201
202+ def __order_twopledict (self , twopledict : rdf .RdfTwopleDictionary ) -> Generator [tuple [str , list [rdf .RdfObject ]]]:
203+ _items_with_sorted_objs = (
204+ (_pred , sorted (_obj_set , key = _obj_ordering_key ))
205+ for _pred , _obj_set in twopledict .items ()
206+ if _pred not in _PREDICATES_RENDERED_SPECIAL
207+ )
208+ yield from sorted (
209+ _items_with_sorted_objs ,
210+ key = lambda _item : _obj_ordering_key (_item [1 ][0 ]),
211+ )
212+
195213 def __obj (self , obj : rdf .RdfObject ) -> None :
196214 if isinstance (obj , str ): # iri
197215 # TODO: detect whether indexcard?
@@ -201,13 +219,15 @@ def __obj(self, obj: rdf.RdfObject) -> None:
201219 with self .__hb .nest ('article' , attrs = {'class' : 'Browse__object' }):
202220 self .__iri_link_and_labels (obj )
203221 elif isinstance (obj , frozenset ): # blanknode
204- if (RDF .type , RDF .Seq ) in obj :
222+ if _is_jsonapi_link_obj (obj ):
223+ self .__jsonapi_link_obj (obj )
224+ elif _is_sequence_obj (obj ):
205225 self .__sequence (obj )
206226 else :
207227 self .__blanknode (obj )
208228 elif isinstance (obj , rdf .Literal ):
209229 self .__literal (obj , is_rdf_object = True )
210- elif isinstance (obj , ( float , int , datetime . date ) ):
230+ elif isinstance (obj , _PRIMITIVE_LITERAL_TYPES ):
211231 self .__literal (rdf .literal (obj ), is_rdf_object = True )
212232 elif isinstance (obj , rdf .QuotedGraph ):
213233 self .__quoted_graph (obj )
@@ -250,31 +270,52 @@ def __sequence(self, sequence_twoples: frozenset[rdf.RdfTwople]) -> None:
250270
251271 def __quoted_graph (self , quoted_graph : rdf .QuotedGraph ) -> None :
252272 with self .__quoted_data (quoted_graph .tripledict ):
253- self .__render_subj (quoted_graph .focus_iri ) # , start_collapsed=True)
273+ self .__render_subj (
274+ quoted_graph .focus_iri ,
275+ should_link_details = (quoted_graph .focus_iri not in self .response_focus .iris )
276+ )
254277
255278 def __blanknode (self , blanknode : rdf .RdfTwopleDictionary | frozenset ) -> None :
256279 _twopledict = (
257280 blanknode
258281 if isinstance (blanknode , dict )
259282 else rdf .twopledict_from_twopleset (blanknode )
260283 )
261- with self .__hb .nest ('details' , attrs = {
262- 'open' : '' ,
263- 'class' : 'Browse__blanknode Browse__object' ,
264- 'style' : self ._hue_turn_css (),
265- }):
266- self .__hb .leaf ('summary' , text = '(blank node)' )
284+ with (
285+ self ._hue_turn_css () as _hue_turn_style ,
286+ self .__hb .nest ('details' , attrs = {
287+ 'open' : '' ,
288+ 'class' : 'Browse__blanknode Browse__object' ,
289+ 'style' : _hue_turn_style ,
290+ }),
291+ ):
292+ with self .__hb .nest ('summary' ):
293+ for _type_iri in _twopledict .get (RDF .type , ()):
294+ self .__compact_link (_type_iri )
267295 self .__twoples (_twopledict )
268296
297+ def __jsonapi_link_obj (self , twopleset : frozenset [rdf .RdfTwople ]) -> None :
298+ _iri = next (
299+ (str (_obj ) for (_pred , _obj ) in twopleset if _pred == RDF .value ),
300+ '' ,
301+ )
302+ _text = next (
303+ (_obj .unicode_value for (_pred , _obj ) in twopleset if _pred == jsonapi .JSONAPI_MEMBERNAME ),
304+ '' ,
305+ )
306+ with self .__nest_link (_iri , attrs = {'class' : 'Browse__blanknode Browse__object' }) as _a :
307+ _a .text = _ ('link: %(linktext)s' ) % {'linktext' : _text }
308+
269309 def __split_iri_pre (self , iri : str ) -> None :
270310 self .__hb .leaf ('pre' , text = '\n ' .join (self .__iri_lines (iri )))
271311
272312 @contextlib .contextmanager
273- def __visiting (self , iri : str ) -> Iterator [ None ]:
313+ def __visiting (self , iri : str ) -> Generator [ str ]:
274314 assert iri not in self .__visiting_iris
275315 self .__visiting_iris .add (iri )
276316 try :
277- yield
317+ with self .__hb .deeper_heading () as _h_tag :
318+ yield _h_tag
278319 finally :
279320 self .__visiting_iris .remove (iri )
280321
@@ -295,27 +336,35 @@ def __iri_link_and_labels(self, iri: str) -> None:
295336 for _text in self .__iri_thesaurus_labels (iri ):
296337 self .__literal (_text )
297338
298- def __nest_link (self , iri : str ) -> contextlib .AbstractContextManager [Element ]:
339+ def __nest_link (self , iri : str , attrs : dict [ str , str ] | None = None ) -> contextlib .AbstractContextManager [Element ]:
299340 _href = (
300341 iri
301342 if _is_local_url (iri )
302343 else trove_browse_link (iri )
303344 )
304- return self .__hb .nest ('a' , attrs = {'href' : _href })
345+ return self .__hb .nest ('a' , attrs = {** ( attrs or {}), 'href' : _href })
305346
306347 def __compact_link (self , iri : str ) -> Element :
307348 with self .__nest_link (iri ) as _a :
308- _a .text = self .iri_shorthand .compact_iri (iri )
349+ _shorthanded = self .iri_shorthand .compact_iri (iri )
350+ if (_shorthanded == iri ) and _is_local_url (iri ):
351+ _shorthanded = f'/{ iri .removeprefix (settings .SHARE_WEB_URL )} '
352+ _a .text = _shorthanded
309353 return _a
310354
311- def __nest_card (self , tag : str ) -> contextlib .AbstractContextManager [Element ]:
312- return self .__hb .nest (
313- tag ,
314- attrs = {
315- 'class' : 'Browse__card' ,
316- 'style' : self ._hue_turn_css (),
317- },
318- )
355+ @contextlib .contextmanager
356+ def __nest_card (self , tag : str ) -> Generator [Element ]:
357+ with (
358+ self ._hue_turn_css () as _hue_turn_style ,
359+ self .__hb .nest (
360+ tag ,
361+ attrs = {
362+ 'class' : 'Browse__card' ,
363+ 'style' : _hue_turn_style ,
364+ },
365+ ) as _element ,
366+ ):
367+ yield _element
319368
320369 def __iri_thesaurus_labels (self , iri : str ) -> list [str ]:
321370 # TODO: consider requested language
@@ -331,10 +380,15 @@ def __iri_thesaurus_labels(self, iri: str) -> list[str]:
331380 _labels .update (_twoples .get (_pred , ()))
332381 return shuffled (_labels )
333382
334- def _hue_turn_css (self ) -> str :
335- _hue_turn = (self .__last_hue_turn + _PHI ) % 1.0
383+ @contextlib .contextmanager
384+ def _hue_turn_css (self ) -> Generator [str ]:
385+ _prior_turn = self .__last_hue_turn
386+ _hue_turn = (_prior_turn + _PHI ) % 1.0
336387 self .__last_hue_turn = _hue_turn
337- return f'--hue-turn: { _hue_turn } turn;'
388+ try :
389+ yield f'--hue-turn: { _hue_turn } turn;'
390+ finally :
391+ self .__last_hue_turn = _prior_turn
338392
339393 def _queryparam_href (self , param_name : str , param_value : str | None ) -> str :
340394 _base_url = self .response_focus .single_iri ()
@@ -367,7 +421,7 @@ def __iri_subheaders(self, iri: str) -> None:
367421 for _label in _labels :
368422 self .__literal (_label )
369423
370- def __iri_lines (self , iri : str ) -> Iterator [str ]:
424+ def __iri_lines (self , iri : str ) -> Generator [str ]:
371425 (_scheme , _netloc , _path , _query , _fragment ) = urlsplit (iri )
372426 yield (
373427 f'://{ _netloc } { _path } '
@@ -389,3 +443,25 @@ def _append_class(el: Element, element_class: str) -> None:
389443
390444def _is_local_url (iri : str ) -> bool :
391445 return iri .startswith (settings .SHARE_WEB_URL )
446+
447+
448+ def _is_sequence_obj (obj : rdf .RdfObject ) -> bool :
449+ return (
450+ isinstance (obj , frozenset )
451+ and (RDF .type , RDF .Seq ) in obj
452+ )
453+
454+
455+ def _is_jsonapi_link_obj (obj : rdf .RdfObject ) -> bool :
456+ return (
457+ isinstance (obj , frozenset )
458+ and (RDF .type , jsonapi .JSONAPI_LINK_OBJECT ) in obj
459+ )
460+
461+
462+ def _obj_ordering_key (obj : rdf .RdfObject ) -> tuple [bool , ...]:
463+ return (
464+ not isinstance (obj , (rdf .Literal , * _PRIMITIVE_LITERAL_TYPES )), # literal values first
465+ not isinstance (obj , str ), # iris next
466+ _is_jsonapi_link_obj (obj ), # jsonapi link objects last
467+ )
0 commit comments