Compare commits

...

185 Commits

Author SHA1 Message Date
Matt Mascarenhas 14dafa4abe cinera.c: Fix stack-use-after-return segfault
The ClashResolver, or a memory_book variable therein, triggered a
stack-use-after-return segfault. Fixed by initialising the ClashResolver
in main() and passing it to InitClashResolver() by pointer, rather than
initialising it in InitClashResolver().
2024-03-12 13:03:08 +00:00
Matt Mascarenhas 213bb2f882 cinera.c: Fix colouring of topic dots
The colour computed for topic dots and put into cinera_topics.css in HSL
format has its lightness value modified depending on whether it's on a
dark or light background. Web browsers do not tell us an element's
computed colour in HSL format, but in RGB, so we would need to convert
it from RGB to HSL when setting the lightness. Previously, this
conversion was busted, calculating too small a value for the saturation.

This commit obviates the need for any RGB→HSL conversion by writing the
hue and saturation values as attributes to the elements, which the
function responsible for setting the lightness may use directly.
2024-02-21 20:52:34 +00:00
Matt Mascarenhas 77aec74483 cinera_player_post.js: Fix fullscreen handling
It is possible to bring our player out of fullscreen mode without using
the provided views menu or keyboard shortcut, e.g. by pressing Escape.
Doing so leaves our state in the SUPERTHEATRE view, which omits
SUPERtheatre mode itself from the views menu. This commit fixes this by
switching our state to the THEATRE view.

Thanks to Aske Bisgaard Vammen for the report.
2024-01-30 15:41:30 +00:00
Matt Mascarenhas 6852d06e04 cinera_player_pre.js: Unset focused menu
When hiding any menu, set this.MenusFocused.MenuID = menu_id.UNSET, to
let hovering over the markers focus them after hiding the link menu.
2023-03-25 01:51:40 +00:00
Matt Mascarenhas 52d6d989f8 cinera_player_pre.js: Fix scoping of "this" 2023-03-25 01:31:13 +00:00
Matt Mascarenhas df93674bf7 cinera: Increase precision to milliseconds
This commit simply adds millisecond precision of timecodes. It includes
small changes to hmmlib.h, cinera.c and the frontend JS files.
2023-03-25 00:04:34 +00:00
Matt Mascarenhas 026585e50b cinera: Fix mobile scrolling and centring
This commit fixes spurious scrolling in DeriveReliableWindowDimensions()
on mobile. It also fixes centring of Vimeo videos on mobile.

In addition, it replaces some deprecated JavaScript:
•   window.orientation → screen.orientation.type
•   window.onorientationchange → screen.orientation.onchange
2023-03-21 19:34:24 +00:00
Matt Mascarenhas 9d5f0f9146 cinera_player_pre.js: Fix scoping of "this" 2023-03-18 01:54:42 +00:00
Matt Mascarenhas a7694d4c3b cinera: Improve keyboard controls and scrolling
This commit adds keyboard navigation of the indices, documented in the
"Help" text box. It also improves scrolling of menus to follow progress
through the video, with the ability to centre the scrolling around a
range of references. Finally in this UI work, it enables the keyboard
and mouse to work more cooperatively.

Other changes:

• Added a "Clear" so the player's initial sizing happens invisibly.
• Fixed getBackgroundColourRGB() to handle both rgb() and rgba().
• Deduplicated code, including spurious querySelectorAll() calls.
• Moved global variables into the Player object.
2023-03-18 01:27:05 +00:00
Matt Mascarenhas c9bf96c7aa cinera_player_pre.js: Improve sizing and scrolling
This commit fixes the sizing of the player's menus to not protrude below
the player.

It also adds scrolling of the references menu to the first menu item
cited in the newly-current timestamp, and fixes scrolling of the markers
container when switching to a timestamp after the user has scrolled the
container away from its default position.
2023-02-20 20:29:16 +00:00
Matt Mascarenhas 44a5008aa7 cinera: Resolve output location clashes
This commit adds support for tracking and resolving clashes of output
locations. Previously open-ended clash dependency chains could passively
resolve across multiple Cinera invocations (undesirable, but possible);
whereas closed-loop dependency chains could not resolve without the user
manually resolving them by moving one clash to a temporary location (not
acceptable). This new support handles such clashes in one fell swoop.

Other smaller changes in this commit:

•   Change message_control to use memory_book
•   Fix deletion of old .index files when reorganising projects
•   Fix deletion of moved entries when subdividing projects
2023-02-10 23:12:21 +00:00
Matt Mascarenhas 6576a91d11 cinera: Support message deduplication
This commit adds support for deduplication of messages emitted by
PrintEdit(). It's a first pass, only supporting single-line messages and
ones no larger than 512 bytes.

It also fixes StringContains() to skip strings shorter than the search
substring, and emits a more sensible error on empty "output" value.
2023-01-13 20:31:44 +00:00
Matt Mascarenhas 1280142d45 cinera.c: Clean up string copy functions
This commit replaces ClearCopyStringNoFormat() with
ClearCopyStringNoFormatOrTerminate(). All calls to that function
passed an implicitly 0-terminated string as the destination, meaning
that we don't need to 0-terminate them and, in not doing, we free up a
byte to store data.

It also deduplicates code as CopyBytes(), and does a little bounds
checking before actually trying to copy.
2023-01-13 12:01:26 +00:00
Matt Mascarenhas 6b09247cd2 cinera.c: Enable default closed captions
This commit adds support for configurably enabling closed captions by
default. Set the "default_cc_lang" in a config file, or "cc_lang" in the
[video] node of an .hmml file. Once set, videos which contain captions
for this langage will be initialised with these captions enabled.

New config setting:
    default_cc_lang

New [video] node field:
    cc_lang
2023-01-11 19:24:41 +00:00
Matt Mascarenhas 554f7393ff cinera_search_pre.js: Fix scrolling bug
This commit fixes ResizeFunction() to scroll back to the original Y
position, rather than the nexus (i.e. the grid or list), after resizing
the grid as a result of the window having been resized.
2023-01-10 12:39:06 +00:00
Matt Mascarenhas e8ed2f0143 cinera.c: Fix buffer overflow copying "output"
This is a hotfix made just to allow using a max-length "output" value.
Next commit should fix buffer overflows for all copies to implicitly
null-terminated destinations.
2022-12-15 20:23:30 +00:00
Matt Mascarenhas ac4b155e73 cinera.c: Add Donorbox as default support platform 2022-12-06 18:22:06 +00:00
Matt Mascarenhas 0f15957cb5 cinera.c: Abbreviate names better
This commit makes the auto-derivation of name abbreviations code handle
quoted nicknames and lower-cased surname prefixes (e.g. du Pré). It also
upstreams the abbreviation to config parse time, and introduces config
fields in the person scope to allow overwriting the auto-derived ones.

Bug fixes:
    •   Fix null pointer dereference in InsertProjectIntoDB(), due to a
        rogue WriteFromByteToPointer() call left in when removing the
        _TopLevel() database modification functions in v0.10.14
    •   Fix config file locking by closing all config files on detecting
        a config file change, even if we had no prior working config
    •   Increase IncludesSearch buffer size from 2 to 4KB

New config fields in the person scope:
    •   abbrev_initial
    •   abbrev_given_or_nickname
    •   abbrev_dotted_initial_and_surname
2022-11-25 21:12:59 +00:00
Matt Mascarenhas 68d1469212 cinera.c: Add Ko-fi as a default support platform 2022-10-13 20:21:48 +01:00
Matt Mascarenhas f321a6b6bf Update README.md 2022-09-23 18:39:01 +01:00
Matt Mascarenhas 6ea5cb6d22 Update README.md 2022-09-23 17:47:12 +01:00
Matt Mascarenhas 0825cbb5ff Update README.md 2022-09-23 17:42:18 +01:00
Matt Mascarenhas 6e9c7edd1d Update README.md 2022-09-23 17:39:07 +01:00
Matt Mascarenhas b8f143e350 cinera: Add database structure specification
This commit aims to generalise database structures such that any block
may be traversed without the need for specific functions for each type.
Concretely, it replaces functions like SkipAsset() and SkipProject()
with a generic SkipHeaderedSectionRecursively(). The change also paves
the way for the plethora of new block types due in CINERA_DB_VERSION 6.
2022-09-19 17:22:19 +01:00
Matt Mascarenhas f60cf3087f cinera: Lock database and config files
This commit aims to help Cinera instances coordinate themselves by
throwing an error upon being instructed to use a database or config
file currently in use by another instance.
2022-09-16 16:10:19 +01:00
Matt Mascarenhas 1cf703e346 cinera: Fully handle auto-numbering
•   Renumber affected entries when inserting / deleting an entry of an
    auto-numbered project
•   Prohibit an entry from sharing an "output" with another entry
•   Prohibit an entry's "output" from containing a slash
•   Fix entry HTML deletion to use OutputLocation, not BaseFilename
2022-08-30 19:15:44 +01:00
Matt Mascarenhas 26e304f62c cinera: Add IRC unfurl tags and GitHub sponsors
New config field:
    instance_title
2022-08-23 22:36:20 +01:00
Matt Mascarenhas 8dffa513cc cinera: Add tag __CINERA_PROJECT_LINEAGE _ 2022-08-21 19:15:10 +01:00
Matt Mascarenhas e17c3aa78c cinera.c: Handle overlong "output" attribute 2022-08-03 22:56:24 +01:00
Matt Mascarenhas 84a35b9ece cinera: Add numbering and fix sizes
New config options:
    numbering_filename_prefix (string)
        Defaults to $project
    numbering_method
        auto
        filename_derived (default)
        hmml_specified
    numbering_start (number)
        May be used to 0-index the set
    numbering_unit (renamed from "unit")
    numbering_zero_pad (boolean)

This commit also fixes the size of the "?" help button and category dots
when the wider site uses box-sizing: border-box
2022-08-01 20:58:55 +01:00
Matt Mascarenhas ef9937e95b hmmlib.h: Add "number" parameter to [video] node 2022-08-01 20:33:38 +01:00
Matt Mascarenhas 652d8f186f Revert "Introduce threading and clean some code"
This reverts commit f7a6df994f.
2022-07-21 20:04:50 +01:00
Matt Mascarenhas 91c33c580b cinera.c: Add a break to a switch case 2022-07-21 19:50:18 +01:00
Matt Mascarenhas f7a6df994f Introduce threading and clean some code
This is an unversioned interim commit with unfinalised code.

The threading code establishes job queues.
The code cleanup removes various _TopLevel functions pertaining to
database operations.
2022-05-20 17:32:28 +01:00
Matt Mascarenhas b6fb755d29 cinera.css: Set margin on search page links
This commit enables Google's Mobile Usability checker to proceed beyond
its quick initial validation, after finding the previous fixes failed.
2021-10-08 15:18:23 +01:00
Matt Mascarenhas d036145b7f cinera.css: Let tall menus scroll 2021-10-06 17:38:18 +01:00
Matt Mascarenhas 60615322dd cinera.css: Apply min-height more broadly
This commit enables Google's Mobile Usability checker to proceed beyond
its quick initial validation.
2021-10-06 17:12:26 +01:00
Matt Mascarenhas 2deb244cce cinera.css: Set min-height on search page links
This commit tries to fix a "Clickable elements too close together"
error:
https://support.google.com/webmasters/answer/9063469#touch_elements_too_close
2021-10-06 16:38:21 +01:00
Matt Mascarenhas 4dc034ecc3 cinera.c: Use html_title in Welcome / End markers 2021-10-06 15:45:19 +01:00
Matt Mascarenhas ee788b0c30 cinera_player_pre.js: Fix YouTube initialisation
Initialise the player if the YouTube API loads before us
2021-09-01 16:38:23 +01:00
Matt Mascarenhas c839e2ed85 cinera.c: Add "direct" video support
Fixes:
• Rename Annotation → Timestamp in cinera_player_*.js
• Redo VideoIsPrivate() to only return once, at the end
2021-07-07 16:33:54 +01:00
Matt Mascarenhas 012d1608a0 cinera.c: Mention "vimeo" in the help text
Also remove HMMLIB_MAJOR_VERSION #defined out code
2021-06-28 13:32:15 +01:00
Matt Mascarenhas 97806c2c9c cinera_player_post.js: s/setTime/setTimeThenPlay
Thanks to @nairou on twitter for the report
2021-06-27 19:13:18 +01:00
Matt Mascarenhas dd33bb49b3 cinera_player_pre.js: Add Vimeo support
Fixes

• Fix fullscreen
• Pause whenever / however following a reference or credit URL
2021-06-23 15:13:41 +01:00
Matt Mascarenhas e39a09c0ad cinera: Code clean-up
• Make vod_platform an enum
• Unionise config_int_pair and config_bool_pair in config_pair
2021-06-17 13:32:53 +01:00
Matt Mascarenhas 744515dac4 cinera_clear.js: Fix the placement of the clear 2021-06-11 17:39:48 +01:00
Matt Mascarenhas 209979f63a cinera.js: Add a "Clear" element
To use it, source the cinera_clear.js file at the position in the HTML
file to be obscured while further processing occurs. (Make sure not to
"defer" it.)

After processing is complete, call FlipClear()
2021-06-11 16:38:38 +01:00
Matt Mascarenhas 12b06812bf cinera.js: Improve style
•   Do not modify visibility of list view during resize
•   Stabilise player sizing across fullscreen toggle and resize
2021-06-11 14:03:13 +01:00
Matt Mascarenhas 373195069a cinera.css: Use rem unit for font-size 2021-06-09 16:30:06 +01:00
Matt Mascarenhas 07795b7cbd cinera.c: Fix segfault on partial hmmlib1 credit 2021-06-07 13:19:03 +01:00
Matt Mascarenhas b0501d20ac cinera.c: Fix sizeof() on a string literal 2021-05-31 22:39:25 +01:00
Matt Mascarenhas af1bff4218 cinera: s/StringLength()/sizeof()-1 on string lits 2021-05-31 21:01:34 +01:00
Matt Mascarenhas 63ab9d5bc2 cinera: Remove memory_book_type 2021-05-31 20:48:58 +01:00
Matt Mascarenhas 9cdb3b96d9 Revert cinera.c: Make ResolvePath() use realpath()
This reverts commit 61fa4e4808.
2021-05-31 20:35:18 +01:00
Matt Mascarenhas 61fa4e4808 cinera.c: Make ResolvePath() use realpath() 2021-05-31 20:10:04 +01:00
Matt Mascarenhas cf51ba24e3 cinera.c: Fix role-related bugs
•   Fix segfault due to unconfigured role
•   Add credit scope to the root scope's TypeSpec, permitting
        credit = "$owner" { role = "host"; }
    to be absorbed by all projects
•   Make "unfound role" errors WaitForInput()
2021-05-31 19:20:52 +01:00
Matt Mascarenhas a6a9653306 hmmlib2 2.0.12: Clamp ref->offset to text_len 2021-05-31 18:06:32 +01:00
Matt Mascarenhas c104500020 hmmlib2 2.0.11: Remove static from compound lit
This permits compilation with older versions of gcc
2021-05-30 23:46:37 +01:00
Matt Mascarenhas 0d88c24db6 cinera: Upgrade to hmmlib2
Features:
    User-configurable roles and credits
    Handle SIGINT to quit cleanly and avoid database corruption

Fixes:
    Filesystem event monitoring handles directory creation / deletion
    Fixed buffer overflow when trying to curl in a non-existent quote
2021-05-30 19:39:47 +01:00
Matt Mascarenhas 65fe93fb57 hmmlib2 2.0.10: Rename annotation to timestamp 2021-05-30 18:42:02 +01:00
Matt Mascarenhas b20192e445 hmmlib2 2.0.9: Add ()"% as break_on_punct chars 2021-05-27 16:11:12 +01:00
Matt Mascarenhas 9a31a1b10c hmmlib2 2.0.8: Align marker rules with hmmlib1
Make extended markers - those within [] - only break on whitespace
Add '!' and '…' as break_on_punct chars
2021-05-24 21:31:45 +01:00
Matt Mascarenhas d1440a7ac6 hmmlib2 2.0.7: Add uncredit attribute 2021-05-24 21:06:40 +01:00
Matt Mascarenhas 0f67d4e658 hmmlib2 2.0.6: Make '?' a break_on_punct char 2021-05-23 22:45:59 +01:00
Alex Baines 00fc6c0ae5 hmmlib2 2.0.5: post-text marker fixes
they don't have parameters, and there can be multiple
2021-05-21 00:50:07 +01:00
Alex Baines c0c3558473 hmmlib2: remove stream_username / member 2021-05-11 23:03:22 +01:00
Alex Baines 3f47e62a7e hmmlib2: allow marker as first thing in text node 2021-05-09 11:52:16 +01:00
Alex Baines c911e393d5 hmmlib2: v2.0.2 fixing issues with code204/284/440
also allow / in markers
2021-05-08 23:30:59 +01:00
Matt Mascarenhas 05b7883033 cinera_search_pre.js: Default to the List View
Also fix the 𝑛-ary Grid traversal PrevAscends case when the search page
contains only one project at the top-level.
2021-04-12 18:00:45 +01:00
Alex Baines e4c2ae4ffa remove branch condition that could never be false
fuzzer gets 100% branch coverage now, no crashes/hangs
2021-04-07 18:50:35 +01:00
Alex Baines 1d916415c1 hmmlib2: consecutive marker/ref bug fix
see riscy004 34:28
2021-04-04 20:57:50 +01:00
Alex Baines 678be54e51 hmmlib2: hmmldump -x option for extra output, afl fuzz testing 2021-04-04 20:39:35 +01:00
Alex Baines 1993e9f1dd hmmlib2: re-instsate "offset" for marker/ref + fixes
fixes:
    - indentation
    - parsing bug for marker with both episode + parameter
2021-04-04 13:07:50 +01:00
Alex Baines 968437d263 hmmlib2 single-header possibly working 2021-04-03 15:42:32 +01:00
Alex Baines 67c4942b60 add unfinished hmmlib rewrite "hmmlib2" 2021-03-07 21:59:43 +00:00
Matt Mascarenhas 828fff7a7a cinera.web: Prevent auto-scroll to hidden elements
The search page could auto-scroll to the top when the grid is not
displayed. This commit fixes that problem by ensuring that the grid
element has a width and height before seeking to auto-scroll to it.

Thanks to Leonardo Serafim Eid for the report
2021-02-16 02:52:27 +00:00
Matt Mascarenhas 8283e2a9e5 cinera_player_pre.js: Remove spurious console.log 2021-02-13 00:16:36 +00:00
Matt Mascarenhas 22633794b5 cinera_pre.js: Fix GetOrSetRule() 2021-02-13 00:12:19 +00:00
Matt Mascarenhas 0cfa87ba84 cinera_pre.js: Handle null StyleSheet.href 2021-02-13 00:00:38 +00:00
Matt Mascarenhas 806c9feba8 cinera.web: Fix sizing and CSS rule setting
• Max{Height,Width}OfElement() should now produce saner values
• Video can now resize upon orientation change on mobile
• Search grid now sizes to the correct area
• CSS rule setting only tries to touch local stylesheets
2021-02-12 23:48:02 +00:00
Matt Mascarenhas b6fff3cc24 cinera_search_post.js: Fix auto-scrolling
Pass CineraProps.IsMobile to InitScrollEventListener()
2021-02-10 23:50:43 +00:00
Matt Mascarenhas 20d7e82a66 cinera_pre.js: Harden GetRule() 2021-02-10 23:33:32 +00:00
Matt Mascarenhas 33f6186aa1 cinera.web: Fix player sizing and auto-scrolling
• Video size may be based on the height if the width-based is too tall
• Auto-scrolling improvements:
    • It now cannot oscillate
    • It doesn't happen at all if scrolled to the very top / bottom

Thanks to @Freak_ on freenode for the report
2021-02-10 22:41:46 +00:00
Matt Mascarenhas 2d26562301 cinera_pre.js: Equalise auto-scroll gatherability 2021-02-07 02:01:38 +00:00
Matt Mascarenhas 4ba03f9338 cinera_pre.js: Add sticky-awareness to auto-scroll
This lets the grid view auto-scroll out from under the control bar.
2021-02-07 01:53:05 +00:00
Matt Mascarenhas 7e80017434 cinera.css: Circular traversal buttons 2021-02-05 20:54:29 +00:00
Matt Mascarenhas 29df4df8a4 cinera_search.js: Better traversal button sizing 2021-02-05 20:15:40 +00:00
Matt Mascarenhas 5857a30eaf cinera_search.js: Pin ↑ button to top in landscape 2021-02-05 19:49:00 +00:00
Matt Mascarenhas 0a9d51b0d7 cinera.web: Better fitting and effiency
Better text fitting
Account for the scrollbar width in our max width considerations
Don't bother to compute the grid size merely when toggling the view
2021-02-04 23:19:56 +00:00
Matt Mascarenhas 50dbc7b5c3 cinera.js: Base dims on window.inner{Width/Height} 2021-02-04 01:22:09 +00:00
Matt Mascarenhas e7d70ccb3a cinera_search_pre.js: Compute grid on view toggle 2021-02-04 00:46:43 +00:00
Matt Mascarenhas f519a0977f cinera_search_pre.js: Fix WindowDim declaration 2021-02-04 00:33:17 +00:00
Matt Mascarenhas 545938d766 cinera.web: Fix issues (on mobile) with new layout
Both pages:
    Derive reliable window dimensions ourselves
    Set the orientation from the window dimensions
    Use "box-sizing: content-box" on the help keys, to fix sizing

Search page:
    Support irregular (i.e. non-square) grids
    General sizing fixes
    Fix text fitting when toggling back from List to Grid view
    Fix text fitting when textNode would overflow container
    Correctly compute optimal grid size after orientation change
    Rename .text to .cineraText to avoid CSS selector clash
    Only add click events for the main set of buttons (not its clone)

Player page:
    Size the video and timestamps bar more sensibly
2021-02-04 00:13:55 +00:00
Matt Mascarenhas 2cf8739a60 cinera.web: Fix issues on desktop with new layout
Search page:
    Manually size the grid buttons, and head / tail items

Player page:
    Confine long menus to the player height
    Confine the help documentation to the window size
2021-01-27 21:58:56 +00:00
Matt Mascarenhas eccd02cc71 cinera.css: Fix menu bar padding
Workaround qutebrowser's (QtWebEngine) inability to handle a list of
selectors in the :not() selector
2021-01-25 18:52:38 +00:00
Matt Mascarenhas 3945ac883c cinera v0.8.0: Mobile-friendly layout
Major features:
    Search page: Subdivision grid layout
    Player page: Device orientation-specific layout

Fixes:
    Strip slashes of the URLs: base_url, etc.
    Fix SnipeChecksumAndCloseFile() to not null-terminate the checksum string
    Fix hover background colouration of medium icons of current timestamp
    Fix DeriveLineageWithoutOriginOfProject() to call InitBookOfPointers()
    Fix SortAndAbbreviateSpeakers() to use the person's ID if Name is blank
    More directly display the "unit", if set, in the search results
    Fix VideoIsPrivate() to find the apparently relocated privacyStatus
    Make VideoIsPrivate() default to TRUE for non-youtube
    Output as keywords all topics that do not match "nullTopic"

Deleted asset:
    cinera_search.js

New assets:
    cinera_search_pre.js
    cinera_search_post.js
2021-01-25 18:09:30 +00:00
Matt Mascarenhas 6eeb588adf cinera.c: Gracefully handle database non-creation 2020-06-24 17:34:41 +01:00
Matt Mascarenhas b3470e0f48 cinera.c: Gracefully handle unset env variables 2020-06-24 16:22:50 +01:00
Matt Mascarenhas 2748687839 cinera.c: Sanitise Memory
Fixes / Improvements:

•   Switch all growable arrays to use memory_book, rather than realloc()
•   Lift a bunch of hardcoded string lengths and item counts
•   malloc() the MemoryArena rather than calloc(), thus saving 3 MiB
•   Reorganise HMMLToBuffers() to return from one place, the end
•   Print indexing errors in the same style as config errors / warnings

Diagnoses:

•   Identified sources of "non-freed" memory usage and marked them +MEM.
    This may aid future work on further reducing memory / cycle usage.

New config settings:

•   suppress_prompts boolean
2020-06-24 13:29:18 +01:00
Matt Mascarenhas 6da970d48c cinera.c: Fix leak in GenerateTopicColours() 2020-06-03 18:04:39 +01:00
Matt Mascarenhas 0959fa2774 cinera.c: Fix segfault and event handling
• Segfault was due to a read access violation on an unset entry pointer,
  which in turn was due to stale neighbourhood data. To fix it we simply
  reset the neighbourhood when starting to delete an entry. Additionally
  we now check that those entry pointers are set before accessing them.

• Event handling of the trio of events triggered when vim saves a file.
  We now read in a second set of events while processing the first if we
  were on the verge of processing a deletion. If we get any more events,
  we continue to squash those ones if possible, to always end up having
  seen the entire trio of events associated with a file save, and then
  process it as an insertion / reinsertion, not a deletion.

• Sort the asset landmarks by their offset.

• Change GenerateTopicColours() to initially open cinera_topics.css as
  "r" to search it for the incoming topic, and only if that topic is
  absent reopen it as "a+", thus triggering an IN_CLOSE_WRITE event.
2020-05-30 18:12:54 +01:00
Matt Mascarenhas 7edcecfd40 cinera.c: InitScopeBooks() in PrintHelpConfig() 2020-05-25 01:29:14 +01:00
Matt Mascarenhas bc6f21f71d Make project navigation menus use the right theme
It was just a typo. HMMLToBuffers() passed the global config pointer,
rather than the passed in one, to GenerateNavigation().

Also switch the entire config over to use memory_book style allocation.
This saves us from having to offset pointers when memory gets realloc'd.
2020-05-25 00:26:13 +01:00
Matt Mascarenhas 8aa67a386a Fix the 3-way filter associating
Add the data-searchLocation attribute to the search results
2020-05-17 23:00:12 +01:00
Matt Mascarenhas ce9a0e7635 Prevent file clashes
Config parsing now errors-out when it detects clashes between:
    base_dir and search_location, and global_search_dir
    base_dir and player_location

This prevents us from generating search / project pages of multiple
projects (incl. global_search_dir) at the same location, which would
screw up the asset landmarks.
2020-05-17 22:50:28 +01:00
Matt Mascarenhas 877dbab5bd Fix global search page generation and filter
This change makes GenerateGlobalSearchPage() pass the Config to
BuffersToHTML(), which in turn passes it on to GenerateNavigation(),
thus preventing a null-pointer deference when setting the Theme.

It also fixes the filter by using all three of baseURL, searchLocation
and playerLocation to associate the filter and index elements.
2020-05-17 20:35:04 +01:00
Matt Mascarenhas 9dfdc117be Fix search_ and player_location related bugs
On the search pages use the search_location and player_location, in
addition to the base_url, to associate filter and index entries, such
that the filter actually works.

Fix the .index file location for projects with a search_location. As
this is now consistent, we happen to fix a crash that happened when
changing the search_location.

New config option:

    deny_bespoke_templates
         Indexers may use the "template" attribute in the video node of
         an .hmml file to set a bespoke template for that entry,
         superseding any configured player_template. Setting
         "deny_bespoke_templates" to true prevents this.
2020-05-17 02:04:26 +01:00
Matt Mascarenhas e2ea8fdaf3 cinera.c: Memorable Templates
• Pack all templates before sync'ing the projects
• Delay generation of navigation bars until BuffersToHTML(), and free them
  when switching projects
• Change many variably-lengthed items to use memory_book
• Do not push on multiple support scopes with the same ID
• Prevent crash when trying to snipe an asset checksum into an HTML file
  we were never permitted to create
• Reorganise ReadFileIntoBuffer() and related functions into cinera.c

New Template Tags:

    __CINERA_GLOBAL_NAV__
2020-05-15 02:35:55 +01:00
Matt Mascarenhas 33e590c6da cinera_config.c: Prevent read access crash
Passing an empty string to StripSlashes() would crash.

Also add a conditional around the ctime calls in the Won Build System,
to only run it if it exists.
2020-05-10 21:25:05 +01:00
Matt Mascarenhas 3e0ba0b77b cinera.c: Add <div> to match closing </div> 2020-05-10 18:43:40 +01:00
Matt Mascarenhas 18b39b9f72 cinera.c: Always print entry deletion from DB
Previously we only printed deletion in DeletePlayerPageFromFilesystem().
If, however, the output parameter is set in a .hmml file, we needn't
delete that page from filesystem. What would happen is, we move the
file from one location (delete) to another (insert / append), and only
printed out the insertion / append. This gave the impression that the
old database entry remained, when it had correctly been deleted.
2020-05-10 17:32:44 +01:00
Matt Mascarenhas f20a35e0de cinera.c: Fix buffer sizing crash bug 2020-05-10 17:05:12 +01:00
Matt Mascarenhas 3728c8aaee cinera.c: Mention the default config path in -h 2020-05-09 22:27:40 +01:00
Matt Mascarenhas bd8c6b805d cinera.c: Bump the patch version 2020-05-09 19:18:17 +01:00
Matt Mascarenhas c883bbb971 cinera_config.c: Remove extraneous #endif 2020-05-09 19:16:57 +01:00
Matt Mascarenhas 05518781c4 cinera.c: Config
Database version 5

Major features:
    Config file parsing
    Multiple projects per instance
    Searching across multiple projects
    Art sprites, usable by project, medium and support

Minor features:
    The search results are now sortable
    Optimised the search page sorting

Fixes:
    More reliable file system monitoring
    Well-formed templates are now valid, even with the absence of assets
    Correctly resove ~ in paths
    Gracefully handle the lack of an internet connection
    Renamed "annotator" to "indexer"
    Removed PlayerURLPrefix, in favour of .hmml "output" parameter

Replaced the flags collection with:
    -0 Dry-run mode
    -c Set the config file path
    -e Examine the database
    -v Print version
    -h Print help

New template tags:
    __CINERA_NAV__ dropdown
    __CINERA_NAV__ horizontal
    __CINERA_NAV__ plain
    __CINERA_PROJECT_PLAIN__

Deleted template tags:
    __CINERA_MENUS__
    __CINERA_SCRIPT__
2020-05-09 18:33:25 +01:00
Matt Mascarenhas 9051caa94e cinera: Player page mobile style 2019-03-07 20:16:27 +00:00
Matt Mascarenhas 35b6b803e3 cinera.css: .cineraQueryContainer label margin 2019-03-04 00:08:06 +00:00
Matt Mascarenhas 9278390a38 cinera.c: Put sort button on query box's line 2019-03-03 23:32:58 +00:00
Matt Mascarenhas f5a767ff94 cinera.css: Typo 2019-03-03 01:09:57 +00:00
Matt Mascarenhas d6e633cf3e cinera.css: Search page mobile style
cinera_search.js: Conditionally display the results summary
2019-03-03 00:44:05 +00:00
Alex Baines 21814d1e48 add publication_date and output to hmml metadata 2019-02-05 20:46:52 +00:00
Matt Mascarenhas 9b26d109be cinera.c: Increase AnnotationHeader buffer size 2018-10-26 20:31:37 +01:00
Matt Mascarenhas d37a39af32 cinera.c: Let inotify monitor IN_MOVED_TO events
It seems that rsync triggers these events, so we must handle these in
addition to the IN_CLOSE_WRITE ones triggered by re-saves.

Also fix MakeDir() to correctly make parent directories (i.e. mkdir -p).
2018-10-05 20:57:09 +01:00
Matt Mascarenhas c4ef54c742 cinera.c: Fix template validity checking 2018-09-20 01:13:41 +01:00
Matt Mascarenhas 9d0cdfb488 cinera.c: Add "Packing template" back 2018-09-20 00:43:47 +01:00
Matt Mascarenhas 2eddd7a7c2 cinera.c: Support limitless templates
Previously we had a template tag limit of 16. This commit lifts that
limit to support "unlimited" tags per template.

Skip already offset landmarks most efficiently when adding a new asset.

New template tags:
    __CINERA_SEARCH_URL__
    __CINERA_VOD_PLATFORM__
2018-09-19 20:50:21 +01:00
Matt Mascarenhas 3da53413ce cinera.c: Revved resources
Database version 4

Revving resources involves hashing asset files and appending a query
string to their URLs. Additionally we monitor asset files for changes
and edit their new checksum hash into all HTML files citing them.

This commit also introduces new template tags for assets (listed below)
with which you may instruct Cinera to rev assets of your choice. There
is further information about this in the help (-h) and the README.md

Amongst other minor changes, we now support unset $XDG_CACHE_HOME and
$HOME
    - Thanks to insofaras for wordexp()

New flags:
    -Q Set query string

New template tags:
    __CINERA_ASSET__
    __CINERA_CSS__
    __CINERA_IMAGE__
    __CINERA_JS__

Renamed template tag:
    __CINERA_SEARCH__ (was __CINERA_INDEX__)
2018-09-17 19:06:31 +01:00
Alex Baines 07db5d0397 improve hmmlib makefile when lex missing even more 2018-08-09 20:18:02 +01:00
Alex Baines 82a49f57e9 improve hmmlib makefile when lex missing 2018-08-09 19:55:44 +01:00
Matt Mascarenhas f3b728ee6f cinera_search.js: Mitigate flickering 2018-07-15 23:52:52 +01:00
Matt Mascarenhas 8607ca87ea cinera: Finer-grained search input autofocus 2018-07-05 20:13:25 +01:00
Matt Mascarenhas aa0a8ba327 cinera: Single browser tab and no autofocus mode
This allows search result links to open in the same tab, and prevents
automatic scrolling to the search input box on page load
2018-07-03 15:26:17 +01:00
Matt Mascarenhas 92909a9ee9 cinera.c: Update Casey's support URL 2018-07-02 19:18:18 +01:00
Matt Mascarenhas 8a4a383705 cinera.c: Version bump for faster search 2018-06-23 16:10:55 +01:00
Matt Mascarenhas 6ca1446450 Merge branch 'test_search_perf' 2018-06-23 16:09:02 +01:00
Asaf Gartner 6136a45886 Attempting to speed up rendering without hurting framerate. 2018-06-23 14:32:01 +00:00
Matt Mascarenhas 8c17194180 cinera.c: Correctly set theme 2018-06-13 16:05:21 +01:00
Matt Mascarenhas a1e8efe431 cinera.c: Fix CollationBuffers->ProjectID test 2018-06-12 20:32:56 +01:00
Matt Mascarenhas 966d616b48 cinera.c: Set CollationBuffers.Theme 2018-06-12 20:21:58 +01:00
Matt Mascarenhas 0eeadd560b cinera.c: Add PROJECT_ID and THEME template tags 2018-06-12 20:03:12 +01:00
Matt Mascarenhas 4d495543f5 cinera.c: Add coad and reader ProjectInfo 2018-06-07 18:55:40 +01:00
Matt Mascarenhas 45cf7772d9 cinera.c: Replace pcalc with risc ProjectInfo 2018-06-07 18:38:52 +01:00
Matt Mascarenhas 238427331f cinera.c: Generate index if any files changed
Previously it could fail to generate an index if the processing of the
final file set "Inserted" or "Deleted" to FALSE, even if a prior file
had set it to TRUE
2018-06-04 23:53:28 +01:00
Matt Mascarenhas fc7c28c047 cinera.c: Add :drawing medium
hmmlib: -std=gnu99
2018-06-03 15:22:06 +01:00
Matt Mascarenhas 8ec5475456 cinera.c: Fail earlier on unknown project
HTML-santitise episode title and project name from templates. This won't
always be correct, but I reckon the common case will require it.
2018-05-25 20:05:06 +01:00
Matt Mascarenhas c788fd464e cinera.css: Prevent link scrollbar, just in case 2018-05-22 22:57:39 +01:00
Matt Mascarenhas ce367b00aa cinera: Share link (to current annotation) 2018-05-22 22:43:59 +01:00
Matt Mascarenhas 426cbfccba cinera_player_pre.js: Use YouTube API's timer
This lets users of Firefox 59+ get sub-millisecond accurate performance:
https://developer.mozilla.org/en-US/docs/Web/API/Performance/now
2018-05-15 20:45:03 +01:00
Matt Mascarenhas 845a824d62 cinera.c: Fix fix from 0.5.49 2018-05-14 15:55:49 +01:00
Matt Mascarenhas 525590fa36 cinera.c: Prevent crash when no ProjectInfo->Unit 2018-05-13 17:10:05 +01:00
Matt Mascarenhas d420513e20 cinera_player_pre.js: Allow DOCTYPE html 2018-05-02 00:47:03 +01:00
Matt Mascarenhas e77d208645 cinera.c: Remove spurious file I/O
Don't \-escape anything in the index

Document server's security header (recommended) requirements
2018-05-01 22:05:44 +01:00
Matt Mascarenhas be37ea234a cinera: Replace all inline CSS and JS
We no longer prevent enforcement of strict server security policies (to
be documented)

Add bounds-checking to the Copy string functions, fixing buffer sizes

Fix the marker skipping (e.g. :afk)
2018-04-22 21:57:31 +01:00
Matt Mascarenhas 4a0630beb0 cinera.c: Reference and player tweaks
Different ISBN database, and slightly more flexible BuildReference()
Permit line-breaking on '/' in the references menu

cinera.css and cinera_player*.js:
More subtle "click here to regain focus" - for @insofaras
Persist theatre mode - for @insofaras
Resume in-progress video at previous timecode - for @AsafGartner
2018-04-18 00:05:14 +01:00
Matt Mascarenhas 8d75865cf3 cinera.c: Preserve the case of the markers' text 2018-04-07 06:30:28 +01:00
Matt Mascarenhas 4b9e1a2fa0 cinera.c: Add new reference combination 2018-04-05 22:25:23 +01:00
Matt Mascarenhas 1efd808783 cinera.c: Distinguish speakers from chat comments
It treats co-hosts and guests differently from chat commenters, styling
and categorising annotations for them such that their contributions
don't come under the "Chat comment" medium

Also do some essentially cosmetic code compression of the marker cases
and other things

cinera_player_pre.js: Make the credits menu initially focus the host's
person if they have no support, rather than the first credited person
who has support
2018-04-04 23:39:38 +01:00
Matt Mascarenhas 14afdc044d cinera.c: Add Pseudonym73 credentials 2018-04-02 21:52:45 +01:00
Matt Mascarenhas 33cbb5b05f cinera.c: Support private videos
It merely checks the video's privacy status and, if not public, records
its corresponding HMML base filename privately in the .metadata without
generating a player page or a search / table of contents entry for it.
Every four hours it will loop over the .metadata to recheck the privacy
status of any privately recorded entries, and process newly public ones.

Fix template validation to check that the script comes after both the
player and menus (previously it only specified the player).

Fix relocation code to try and remove only the child directories of the
BaseDir, and no shallower (previously it would try and "recursively"
remove directories all the way back to /, obviously not actually getting
there because it would encounter a non-empty directory along the way).

Add "Modes" to the startup printout.

Flags:
    -g Ignore privacy status
2018-04-01 21:58:53 +01:00
Matt Mascarenhas a27e603e6e cinera.c: Add info for Per and Bitwise 2018-03-23 15:33:37 +00:00
Alex Baines 7034ce096b hmmlib.h: don't include stdbool 2018-03-14 23:01:54 +00:00
Matt Mascarenhas de2c632806 cinera.c: Binary search the .metadata
Also optimise out superfluous searches, and relieve IndexToBuffer() of
the need to string-search the .index

Rewrite the table of contents page after deleting an entry

This commit also retains profiling, as a reminder to me how I used it,
and the old linear search code. The timing blocks and the old code may
be deleted in a future commit
2018-03-06 20:40:12 +00:00
Matt Mascarenhas ab598e37e6 cinera_player_pre.js: Reenable YT interaction
Essentially, the previous change prevented the marker progress code from
firing if the user only interacts with the YouTube player directly.

cinera.c: Try to curl the quotes in, rather than straight up deleting
them, if(ShouldFetchQuotes)
2018-02-28 20:18:11 +00:00
Matt Mascarenhas 2053be1969 README.md: Update 2018-02-28 01:40:53 +00:00
Matt Mascarenhas 23e31df4e2 README.md: Update 2018-02-28 01:40:02 +00:00
Matt Mascarenhas 6f99a50f50 cinera_player_pre.js: Only resume() if playing 2018-02-28 01:25:50 +00:00
Matt Mascarenhas e7aefbada0 cinera.css: Marker and categories style
cinera_player_pre.js: Episode keyboard navigation. Also swap out A for
K, and D for J
cinera_player_pre.js: Handle the case in onRefChanged() in which the
filter_container or filterState is not present
cinera.c: Refetch quotes when processing a set of annotations >60 mins
after the last fetch

Flags:
    -w Force quote cache rebuild
2018-02-28 01:04:06 +00:00
Matt Mascarenhas 2cac3ed03b cinera.c: Add <!-- __CINERA_CUSTOMn__--> tags
Also compress the template code

cinera_player_pre.js: Fix bug in which onRefChanged() tried to call
player.jumpToNextMarker(); before the player was ready. This could
happen if its first marker's timecode is 0:00 and medium is :afk

Enable Theatre mode to work in containers styled with max-width or
max-height
2018-02-23 23:36:42 +00:00
Matt Mascarenhas c5bc487313 cinera.c: Prev / Next Episode Links
Also fix the inotify loop, augment the index (the .metadata files) to
contain data useful for insobot, and futureproof UpgradeDB()

Flags:

    * -e Examine Index
        Just pop an -e on the end of your usual list of flags
    * -u Update Interval (was -U)
2018-02-21 21:50:23 +00:00
Alex Baines bbd1d657a7 hmmlib: add custom attributes 2018-02-21 20:05:03 +00:00
Matt Mascarenhas c7762a0f7d cinera.css: Let cinera__*.css colour the indicator 2018-02-12 22:39:27 +00:00
Matt Mascarenhas 87e63a54e7 cinera.css: Index a:visited style 2018-02-12 21:54:28 +00:00
Matt Mascarenhas 9d9d8a6332 cinera.c: Validate timecode chronology
Also correctly skip .hmml files for credentials-related reasons
2018-02-05 23:57:17 +00:00
Matt Mascarenhas 1603245fad cinera.c: HTTPS for Miblo's HomepageURL 2018-02-04 21:52:32 +00:00
Matt Mascarenhas bd590d4c07 cinera.c: Miblo pledge credentials 2018-02-04 20:04:01 +00:00
Matt Mascarenhas e5d592dc11 cinera_search.js: Remove console.log 2018-01-28 21:57:35 +00:00
Matt Mascarenhas 0f27c96b29 cinera_search.js: Prevent crashes caused by | 2018-01-28 21:56:40 +00:00
Matt Mascarenhas 3d7add5b66 cinera: Handle sorting state separately from UI
Also add user-select: none
2018-01-21 19:30:54 +00:00
Matt Mascarenhas d7d27f59b7 cinera: Sortable index 2018-01-21 18:59:09 +00:00
Matt Mascarenhas b8013133b9 cinera: Fix theatre and add GUI clickable for it 2018-01-17 20:15:00 +00:00
Matt Mascarenhas f270ee2afa cinera: Theatre Mode
This also changes the hardcoded, unintegrated output to wrap cineraMenus
and cineraPlayerContainer in a <div>, as newly reflected in the example
template_player.html
2018-01-15 21:52:24 +00:00
Matt Mascarenhas 5e7029d2b0 cinera: Tweak filter
Introduce nullTopic, visually represent the media in annotations and
default to "exclusive" mode
2018-01-15 00:03:11 +00:00
Matt Mascarenhas 78861a1c78 cinera.css: Better behaving search .css
Keeps the results looking more consistent between various display widths
2018-01-12 23:26:07 +00:00
Matt Mascarenhas 93fa77b055 cinera.c: Correct template buffers
The template buffers no longer contain indentation spaces at the start
or a \n at the end. This removes any weirdness from template composition

Also remove the cleanup code added in 0.5.22
2018-01-08 22:59:36 +00:00
35 changed files with 40107 additions and 6178 deletions

232
README.md
View File

@ -3,133 +3,143 @@ deployment
## Cinera
### Download, and prepare the parser
1. `git clone git@gitssh.handmade.network:Annotation-Pushers/Annotation-System.git`
2. `cd Annotation-System/hmmlib`
3. `make`
4. `cp hmml.a hmmlib.h ../cinera/`
5. `cd ../cinera/`
Note: For each parser update, remember to make and copy it into place
### Install Dependencies
### Install the dependencies
1. curl
### Download, and copy the parser into place
1. `git clone https://git.handmade.network/Annotation-Pushers/Annotation-System.git`
2. `cd Annotation-System/cinera`
3. `cp ../hmmlib2/hmmlib.h .`
Note: For each parser update, remember to copy it into place.
### Build
1. `$SHELL cinera.c`
### Configure
cinera -h
This documents the configuration file format and default settings.
### Configure the server
If you enforce a strict Content Security Policy and X-Frame-Options in your
server configuration as recommended by [Security
Headers](https://securityheaders.com/), you may enable _Cinera_ to function by
making two small tweaks:
add_header Content-Security-Policy "default-src … https://www.youtube.com https://s.ytimg.com";
add_header X-Frame-Options "ALLOW-FROM https://www.youtube.com";
Note: For more information about these and other security headers, see Scott
Helme's articles [Content Security Policy - An Introduction](https://scotthelme.co.uk/content-security-policy-an-introduction/)
and [Hardening your HTTP response headers](https://scotthelme.co.uk/hardening-your-http-response-headers/#x-frame-options).
### Run
#### Single Edition operation
cinera test.hmml
This simply generates an HTML file (and updates `cinera_topics.css` if needed)
from `test.hmml` and outputs to `out.html`
#### Project Edition operation
cinera -p ProjectID
Setting the ProjectID with the `-p` flag triggers Project Edition. In this
edition `cinera` monitors the Project Input Directory for new, edited and
deleted .hmml files, and generates one table of contents / search page and a
player page each for valid sets of annotations (or removes them, if needed).
By default all directories - input and output - are set to the current working
directory. Typical operation will involve setting these flags:
-d Project Input Directory, the directory where the .hmml files reside
-r Root Directory, path shallower than or equal to the CSS, Images and JS
directories
-u Root URL, corresponding to the Root Directory (optional if the Output
Base Directory resides in the Root Directory)
-b Output Base Directory, location of the table of contents / search page
-t Player Template Location
-x Index Template Location
#### Integration
CINERA_MODE=INTEGRATE cinera test.hmml
This will integrate into `template_player.html` (configurable with -t) the
player and related elements generated from `test.hmml` and output to
`out_integrated.html`
Feel free to play with `template_player.html` to your heart's content. If you do
anything invalid, `cinera` will tell you what's wrong
cinera -c /path/to/config.conf
#### Templates
Valid tags:
- `<!-- __CINERA_PROJECT__ -->` _the project's full name_
- `<!-- __CINERA_TITLE__ -->` _the day / episode name, intended to be used
inside your own `<title>` element, but may be used wherever and as many times
as you want on your page_
*Index Template*
- `<!-- __CINERA_INCLUDES__ -->` _the necessary `.css` and `.js` files_
- `<!-- __CINERA_INDEX__ -->` _the table of contents, and search functionality_
*(Global) Search Template*
- `<!-- __CINERA_INCLUDES__ -->` _to put inside your own `<head></head>`_
- `<!-- __CINERA_SEARCH__ -->` _the table of contents and search functionality_
*Player Template*
- `<!-- __CINERA_INCLUDES__ -->` _the necessary `.css` and `.js` files, and charset setting_
- `<!-- __CINERA_MENUS__ -->` _ _the menu bar that typically appears above the player in my samples_
- `<!-- __CINERA_PLAYER__ -->` _the player_
- `<!-- __CINERA_SCRIPT__ -->` _the filter state objects and `.js` file, which must come after both the MENUS and PLAYER tags_
- `<!-- __CINERA_INCLUDES__ -->` _to put inside your own `<head></head>`_
- `<!-- __CINERA_PLAYER__ -->`
*Optional tags available for use in your Player Template*
- `<!-- __CINERA_TITLE__ -->`
- `<!-- __CINERA_VIDEO_ID__ -->`
- `<!-- __CINERA_VOD_PLATFORM__ -->`
*Other tags available for use in any template*
- Asset tags:
- `<!-- __CINERA_ASSET__ path.ext -->`
General purpose tag that outputs the URL of the specified asset
relative to the Asset Root URL (-R)
- `<!-- __CINERA_IMAGE__ path.ext -->`
General purpose tag that outputs the URL of the specified asset
relative to the Images Directory (-i)
- `<!-- __CINERA_CSS__ path.ext -->`
Convenience tag that outputs a `<link rel="stylesheet"...>` node
for the specified asset relative to the CSS Directory (-c), for
use inside your `<head>` block
- `<!-- __CINERA_JS__ path.ext -->`
Convenience tag that outputs a `<script type="text/javascript"...>`
node for the specified asset relative to the JS Directory (-j),
for use wherever a `<script>` node is valid
The path.ext in these tags supports parent directories to locate the
asset file relative to its specified type directory (generic, CSS, image
or JS), including the "../" directory, and paths containing spaces must
be surrounded with double-quotes (\-escapable if the quoted path itself
contains double-quotes).
All these asset tags additionally perform versioning, appending a query string
(-Q) and the file's checksum to the URL. Changes to a file trigger a rehash and
edit of all HTML pages citing this asset.
- Navigation / Menu tags:
- `<!-- __CINERA_NAV__ -->`
This menu will contain only the current project, its siblings and
subprojects of both the aforementioned
- `<!-- __CINERA_GLOBAL_NAV__ -->`
This menu will contain every project in the configuration
These navigation tags additionally take one parameter, e.g.
`<!-- __CINERA_NAV__ horizontal -->`:
- `dropdown`
A dropdown menu, that opens on hover
- `horizontal`
More-or-less the same as `dropdown`, but always open
- `plain`
A straightforward `<ul>` for you to style how you like
- `<!-- __CINERA_PROJECT__ -->`
The project's `html_title` if configured, otherwise its `title`
- `<!-- __CINERA_PROJECT_PLAIN__ -->`
The project's `title`
- `<!-- __CINERA_PROJECT_ID__ -->`
- `<!-- __CINERA_PROJECT_LINEAGE__ -->`
The IDs of all projects from the root to the current project, separated
by a spaced-slash
- `<!-- __CINERA_SEARCH_URL__ -->`
- `<!-- __CINERA_THEME__ -->`
- `<!-- __CINERA_URL__ -->`
- `<!-- __CINERA_CUSTOM0__ -->`
- `<!-- __CINERA_CUSTOM1__ -->`
- `<!-- __CINERA_CUSTOM2__ -->`
- `<!-- __CINERA_CUSTOM15__ -->`
Freeform buffers for small snippets of localised information, e.g. a
single `<a>` element or perhaps a `<!-- comment -->`
They correspond to the custom0 to custom15 attributes in the [video]
node in your .hmml files
0 to 11 may hold up to 255 characters
12 to 15 may hold up to 1023 characters
Feel free to play with templates to your heart's content. If you do anything
invalid, _Cinera_ will tell you what's wrong.
#### Arguments
Usage: ./cinera [option(s)] filename(s)
Usage: ./cinera [option(s)]
Options:
Paths:
-r <root directory>
Override default root directory (".")
-u <root URL>
Override default root URL ("")
-b <base output directory>
Override project's default base output directory (".")
-c <CSS directory path>
Override default CSS directory (""), relative to root
-i <images directory path>
Override default images directory (""), relative to root
-j <JS directory path>
Override default JS directory (""), relative to root
-t <player template location>
Override default player template location ("template_player.html"), relative to root
and automatically enable integration
-x <index template location>
Override default index template location ("template_index.html"), relative to root
and automatically enable integration
-o <output location>
Override default output player location for SINGLE_EDITION ("out.html")
-d <project directory>
Override default project directory (".")
-f
Force integration with an incomplete template
-p <project ID>
Set the project ID, corresponding to the "project" field in the HMML files
-s <style>
Set the style / theme, corresponding to a cinera__*.css file
This is equal to the "project" field in the HMML files by default
-l <n>
Override default log level (0), where n is from 0 (terse) to 7 (verbose)
-m <default medium>
Override default default medium ("programming")
-U <seconds>
Override default update interval ("4")
Options:
-c <config file path>
Set the main config file path
Defaults to: $XDG_CONFIG_HOME/cinera/cinera.conf
-0
Dry-run mode. Parse and print the config, but do not modify the
filesystem
-e
Display (examine) database and exit
-v
display version and exit
-h
display this help
#### Environment Variables
CINERA_MODE=INTEGRATE
Enable integration
Display version and exit
-h
Display this help

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

23
cinera/cinera_clear.js Normal file
View File

@ -0,0 +1,23 @@
// NOTE(matt): To use, source this file inside the element you wish to be obscured while processing occurs.
// After processing is complete, call FlipClear()
var ThisScriptElement = document.currentScript;
var NodeToHide = ThisScriptElement.parentNode;
var CineraClearElement = document.createElement("DIV");
CineraClearElement.style.position = "fixed";
CineraClearElement.style.height = "100%";
CineraClearElement.style.width = "100%";
CineraClearElement.style.zIndex = 64;
var CineraPlacedClear = NodeToHide.appendChild(CineraClearElement);
var Colour = getBackgroundColourRGB(CineraPlacedClear);
CineraPlacedClear.style.background = "rgb(" + Colour.R + ", " + Colour.G + ", " + Colour.B + ")"
document.body.style.overflowY = "hidden";
function
FlipClear(BodyOverflowY)
{
document.body.style.overflowY = BodyOverflowY ? BodyOverflowY : null;
CineraPlacedClear.parentNode.removeChild(CineraPlacedClear);
}

5398
cinera/cinera_config.c Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,151 +1,58 @@
var menuState = [];
var titleBar = document.querySelector(".cineraMenus");
var quotesMenu = titleBar.querySelector(".quotes_container");
if(quotesMenu)
{
menuState.push(quotesMenu);
var quoteItems = quotesMenu.querySelectorAll(".ref");
if(quoteItems)
var baseURL = location.hash ? (location.toString().substr(0, location.toString().length - location.hash.length)) : location;
var CineraProps = {
C: null,
V: views.REGULAR,
Z: null,
X: null,
Y: null,
W: null,
mW: null,
H: null,
mH: null,
P: null,
Display: null,
FlexDirection: null,
JustifyContent: null,
O: null,
IsMobile: IsMobile(),
ScrollX: null,
ScrollY: null,
VODPlatform: null,
};
CineraProps.O = GetRealOrientation(orientations.LANDSCAPE_LEFT, CineraProps.IsMobile);
var MobileCineraContentRuleSelector = ".cinera.mobile .cineraPlayerContainer .markers_container > .markers .marker .cineraContent";
var MobileCineraContentRule = GetOrSetRule(MobileCineraContentRuleSelector);
var MenuContainerRuleSelector = ".cineraMenus > .menu .quotes_container, .cineraMenus > .menu .references_container, .cineraMenus > .menu .filter_container, .cineraMenus > .menu .views_container, .cineraMenus > .menu .link_container, .cineraMenus > .menu .credits_container";
var MenuContainerRule = GetOrSetRule(MenuContainerRuleSelector);
var cinera = document.querySelector(".cinera");
var player = new Player(cinera, onRefChanged);
window.addEventListener("resize", function() {
if(CineraProps.IsMobile)
{
for(var i = 0; i < quoteItems.length; ++i)
{
quoteItems[i].addEventListener("mouseenter", function(ev) {
mouseOverQuotes(this);
})
};
setTimeout(DelayedUpdateSize, 512, player);
}
var quoteTimecodes = quotesMenu.querySelectorAll(".refs .ref .ref_indices .timecode");
for (var i = 0; i < quoteTimecodes.length; ++i) {
quoteTimecodes[i].addEventListener("click", function(ev) {
if (player) {
var time = ev.currentTarget.getAttribute("data-timestamp");
mouseSkipToTimecode(player, time, ev);
}
});
}
var lastFocusedQuote = null;
}
var referencesMenu = titleBar.querySelector(".references_container");
if(referencesMenu)
{
menuState.push(referencesMenu);
var referenceItems = referencesMenu.querySelectorAll(".ref");
if(referenceItems)
else
{
for(var i = 0; i < referenceItems.length; ++i)
{
referenceItems[i].addEventListener("mouseenter", function(ev) {
mouseOverReferences(this);
})
};
var lastFocusedReference = null;
var lastFocusedIdentifier = null;
player.updateSize();
}
});
var refTimecodes = referencesMenu.querySelectorAll(".refs .ref .ref_indices .timecode");
for (var i = 0; i < refTimecodes.length; ++i) {
refTimecodes[i].addEventListener("click", function(ev) {
if (player) {
var time = ev.currentTarget.getAttribute("data-timestamp");
mouseSkipToTimecode(player, time, ev);
}
});
}
}
if(referencesMenu || quotesMenu)
{
var refSources = titleBar.querySelectorAll(".refs .ref"); // This is for both quotes and refs
for (var i = 0; i < refSources.length; ++i) {
refSources[i].addEventListener("click", function(ev) {
if (player) {
player.pause();
}
});
}
}
var filterMenu = titleBar.querySelector(".filter_container");
if(filterMenu)
{
menuState.push(filterMenu);
var lastFocusedCategory = null;
var lastFocusedTopic = null;
var lastFocusedMedium = null;
var filter = filterMenu.parentNode;
var filterModeElement = filter.querySelector(".filter_mode");
filterModeElement.addEventListener("click", function(ev) {
toggleFilterMode();
});
var filterMode = filterModeElement.classList[1];
var filterItems = filter.querySelectorAll(".filter_content");
for(var i = 0; i < filterItems.length; ++i)
screen.orientation.onchange = function() {
if(CineraProps.IsMobile)
{
filterItems[i].addEventListener("mouseenter", function(ev) {
navigateFilter(this);
})
filterItems[i].addEventListener("click", function(ev) {
filterItemToggle(this);
});
setTimeout(DelayedUpdateSize, 512, player);
}
}
var creditsMenu = titleBar.querySelector(".credits_container");
if(creditsMenu)
{
menuState.push(creditsMenu);
var lastFocusedCreditItem = null;
var creditItems = creditsMenu.querySelectorAll(".person, .support");
for(var i = 0; i < creditItems.length; ++i)
else
{
creditItems[i].addEventListener("mouseenter", function(ev) {
if(this != lastFocusedCreditItem)
{
lastFocusedCreditItem.classList.remove("focused");
if(lastFocusedCreditItem.classList.contains("support"))
{
setIconLightness(lastFocusedCreditItem.firstChild);
}
lastFocusedCreditItem = this;
focusedElement = lastFocusedCreditItem;
focusedElement.classList.add("focused");
if(focusedElement.classList.contains("support"))
{
setIconLightness(focusedElement.firstChild);
}
}
})
player.updateSize();
}
};
var supportIcons = creditsMenu.querySelectorAll(".support_icon");
{
for(var i = 0; i < supportIcons.length; ++i)
{
setIconLightness(supportIcons[i]);
}
}
}
var sourceMenus = titleBar.querySelectorAll(".menu");
var helpButton = titleBar.querySelector(".help");
var helpDocumentation = helpButton.querySelector(".help_container");
helpButton.addEventListener("click", function(ev) {
handleMouseOverMenu(this, ev.type);
})
var focusedElement = null;
var focusedIdentifier = null;
var playerContainer = document.querySelector(".cineraPlayerContainer")
var player = new Player(playerContainer, onRefChanged);
window.addEventListener("resize", function() { player.updateSize(); });
document.addEventListener("keydown", function(ev) {
var key = ev.key;
if(ev.getModifierState("Shift") && key == " ")
@ -153,44 +60,17 @@ document.addEventListener("keydown", function(ev) {
key = "capitalSpace";
}
if(handleKey(key) == true && focusedElement)
if(!ev.getModifierState("Control") && player.handleKey(key) == true && player.MenusFocused.Item)
{
ev.preventDefault();
}
});
for(var i = 0; i < sourceMenus.length; ++i)
{
sourceMenus[i].addEventListener("mouseenter", function(ev) {
handleMouseOverMenu(this, ev.type);
})
sourceMenus[i].addEventListener("mouseleave", function(ev) {
handleMouseOverMenu(this, ev.type);
})
};
var testMarkers = playerContainer.querySelectorAll(".marker");
window.addEventListener("blur", function(){
document.getElementById("focus-warn").style.display = "block";
document.addEventListener("fullscreenchange", function() {
if(!document.fullscreenElement && CineraProps.V == views.SUPERTHEATRE)
{
CineraProps.V = views.THEATRE;
localStorage.setItem(player.cineraViewStorageItem, views.THEATRE);
player.updateSize();
}
});
window.addEventListener("focus", function(){
document.getElementById("focus-warn").style.display = "none";
});
var colouredItems = playerContainer.querySelectorAll(".author, .member, .project");
for(i = 0; i < colouredItems.length; ++i)
{
setTextLightness(colouredItems[i]);
}
var topicDots = document.querySelectorAll(".category");
for(var i = 0; i < topicDots.length; ++i)
{
setDotLightness(topicDots[i]);
}
if(location.hash) {
player.setTime(location.hash.startsWith('#') ? location.hash.substr(1) : location.hash);
}

File diff suppressed because it is too large Load Diff

86
cinera/cinera_post.js Normal file
View File

@ -0,0 +1,86 @@
var cineraDropdownNavigation = document.getElementsByClassName("cineraNavDropdown");
for(var i = 0; i < cineraDropdownNavigation.length; ++i)
{
var cineraFamily = cineraDropdownNavigation[i].getElementsByClassName("cineraNavHorizontal")[0];
cineraDropdownNavigation[i].addEventListener("click", function() {
if(cineraFamily.classList.contains("visible"))
{
cineraFamily.classList.remove("visible");
}
else
{
cineraFamily.classList.add("visible");
}
});
cineraDropdownNavigation[i].addEventListener("mouseenter", function() {
cineraFamily.classList.add("visible");
});
cineraDropdownNavigation[i].addEventListener("mouseleave", function() {
cineraFamily.classList.remove("visible");
});
}
var Sprites = document.getElementsByClassName("cineraSprite");
for(var i = 0; i < Sprites.length; ++i)
{
var This = Sprites[i];
var TileX = This.getAttribute("data-tile-width");
var TileY = This.getAttribute("data-tile-height");
var AspectRatio = TileX / TileY;
// TODO(matt): Nail down the desiredness situation. Perhaps respond to:
// width / min-width / height / min-height set in a CSS file
// flexbox layout, if possible
// NOTE(matt): These values are "decoupled" here, to facilitate handling of sizes other than the original
// We'll probably need some way of checking the desired and original of both the X and Y, and pick which one on
// which to base the computation of the other
var DesiredX = TileX;
var DesiredY = DesiredX / AspectRatio;
var Proportion = DesiredX / TileX;
//
////
// NOTE(matt): Size the container and its background image
//
This.style.width = DesiredX + "px";
This.style.height = DesiredY + "px";
var SpriteWidth = This.getAttribute("data-sprite-width");
var SpriteHeight = This.getAttribute("data-sprite-height");
This.style.backgroundSize = SpriteWidth * Proportion + "px " + SpriteHeight * Proportion + "px";
//
////
// NOTE(matt): Pick the tile
//
setSpriteLightness(This);
if(This.classList.contains("dark"))
{
This.style.backgroundPositionX = This.getAttribute("data-x-dark") + "px";
}
if(elementIsFocused(This))
{
This.style.backgroundPositionY = This.getAttribute("data-y-focused") + "px";
}
if(This.classList.contains("off"))
{
This.style.backgroundPositionY = This.getAttribute("data-y-disabled") + "px";
}
else
{
This.style.backgroundPositionY = This.getAttribute("data-y-normal") + "px";
}
//
////
// NOTE(matt): Finally apply the background image
var URL = This.getAttribute("data-src");
This.style.backgroundImage = "url('" + URL + "')";
}

614
cinera/cinera_pre.js Normal file
View File

@ -0,0 +1,614 @@
var orientations = {
PORTRAIT: 0,
LANDSCAPE_LEFT: 90,
LANDSCAPE_RIGHT: -90,
};
function
DeriveReliableWindowDimensions()
{
// https://www.howtocreate.co.uk/tutorials/javascript/browserwindow
var Result = {
X: null,
Y: null,
};
var ScrollPosX = window.scrollX;
var ScrollPosY = window.scrollY;
var DisplaySettings = [];
for(var i = 0; i < document.body.children.length; ++i)
{
var Child = document.body.children[i];
DisplaySettings.push(Child.style.display);
Child.style.display = "none";
}
var Element = document.createElement("div");
Element.style.position = "fixed";
Element.style.top = 0;
Element.style.right = 0;
Element.style.bottom = 0;
Element.style.left = 0;
Element.style.zOffset = -1;
Element.style.opacity = 0;
var ElementInPlace = document.body.appendChild(Element);
Result.X = ElementInPlace.offsetWidth;
Result.Y = ElementInPlace.offsetHeight;
ElementInPlace.remove();
for(var i = 0; i < document.body.children.length; ++i)
{
var Child = document.body.children[i];
Child.style.display = DisplaySettings.shift();
}
ScrollTriggeredInternally = true;
window.scroll(ScrollPosX, ScrollPosY);
return Result;
}
function
GetWindowDim(IsMobile)
{
var Result = {
X: null,
Y: null,
};
if(IsMobile)
{
Result = DeriveReliableWindowDimensions();
}
else
{
Result.X = document.documentElement.clientWidth;
Result.Y = document.documentElement.clientHeight;
}
return Result;
}
function IsVisible(Element, WindowDim) {
var BoundingRect = Element.getBoundingClientRect();
return ((BoundingRect.top >= 0 && BoundingRect.top <= WindowDim.Y) ||
(BoundingRect.bottom >= 0 && BoundingRect.bottom <= WindowDim.Y) ||
(BoundingRect.top < 0 && BoundingRect.bottom > WindowDim.Y))
&& ((BoundingRect.left >= 0 && BoundingRect.left <= WindowDim.X) ||
(BoundingRect.right >= 0 && BoundingRect.right <= WindowDim.X) ||
(BoundingRect.left < 0 && BoundingRect.right > WindowDim.X));
}
function
GetRealOrientation(PreferredLandscape, IsMobile)
{
var Result = screen.orientation.angle;
var WindowDim = GetWindowDim(IsMobile);
if(WindowDim.Y > WindowDim.X)
{
Result = orientations.PORTRAIT;
}
else if(Result == undefined || Result == orientations.PORTRAIT)
{
Result = PreferredLandscape;
}
return Result;
}
var DebugConsoleMessageCount = 0;
function Say(Message)
{
var DebugConsole = document.getElementById("debug-console");
if(DebugConsole)
{
DebugConsole.textContent += DebugConsoleMessageCount++ + ": " + Message + "\n";
DebugConsole.scrollTo(
{
top: DebugConsole.scrollHeight,
behavior: "smooth"
});
}
}
function getBackgroundBrightness(element) {
var colour = getComputedStyle(element).getPropertyValue("background-color");
var depth = 0;
while((colour == "transparent" || colour == "rgba(0, 0, 0, 0)") && depth <= 4)
{
element = element.parentNode;
colour = getComputedStyle(element).getPropertyValue("background-color");
++depth;
}
var rgb = colour.slice(4, -1).split(", ");
var result = Math.sqrt(rgb[0] * rgb[0] * .241 +
rgb[1] * rgb[1] * .691 +
rgb[2] * rgb[2] * .068);
return result;
}
function setSpriteLightness(spriteElement)
{
if(getBackgroundBrightness(spriteElement) < 127)
{
spriteElement.classList.add("dark");
}
else
{
spriteElement.classList.remove("dark");
}
}
function elementIsFocused(Element)
{
var Result = false;
if(Element.classList.contains("focused"))
{
Result = true;
}
while(Element.parent)
{
Element = Element.parent;
if(Element.classList.contains("focused"))
{
Result = true;
break;
}
}
return Result;
}
function focusSprite(Element)
{
if(Element.classList.contains("cineraSprite"))
{
setSpriteLightness(Element);
Element.style.backgroundPositionY = Element.getAttribute("data-y-focused") + "px";
}
for(var i = 0; i < Element.childElementCount; ++i)
{
focusSprite(Element.children[i]);
}
}
function enableSprite(Element)
{
if(Element.classList.contains("focused"))
{
focusSprite(Element);
}
else
{
if(Element.classList.contains("cineraSprite"))
{
setSpriteLightness(Element);
Element.style.backgroundPositionY = Element.getAttribute("data-y-normal") + "px";
}
for(var i = 0; i < Element.childElementCount; ++i)
{
enableSprite(Element.children[i]);
}
}
}
function disableSprite(Element)
{
if(Element.classList.contains("focused"))
{
focusSprite(Element);
}
else
{
if(Element.classList.contains("cineraSprite"))
{
setSpriteLightness(Element);
Element.style.backgroundPositionY = Element.getAttribute("data-y-disabled") + "px";
}
for(var i = 0; i < Element.childElementCount; ++i)
{
disableSprite(Element.children[i]);
}
}
}
function unfocusSprite(Element)
{
if(Element.classList.contains("off"))
{
disableSprite(Element);
}
else
{
if(Element.classList.contains("cineraSprite"))
{
setSpriteLightness(Element);
Element.style.backgroundPositionY = Element.getAttribute("data-y-normal") + "px";
}
for(var i = 0; i < Element.childElementCount; ++i)
{
unfocusSprite(Element.children[i]);
}
}
}
function IsMobile() {
// NOTE(matt): From https://medium.com/simplejs/detect-the-users-device-type-with-a-simple-javascript-check-4fc656b735e1
var Identifier = navigator.userAgent||navigator.vendor||window.opera;
var Result = (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(Identifier)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(Identifier.substr(0,4)));
return Result;
};
function GetRule(SelectorText)
{
// NOTE(matt): Modifying CSS style
// From https://stackoverflow.com/a/566445
// https://usefulangle.com/post/39/adding-css-to-stylesheet-with-javascript
var Result = undefined;
var StyleSheets = document.styleSheets;
var cssRuleCode = document.all ? 'rules' : 'cssRules'; // account for IE and FF
for(var StyleSheetIndex = StyleSheets.length - 1; StyleSheetIndex >= 0; --StyleSheetIndex)
{
var ThisSheet = StyleSheets[StyleSheetIndex];
if(ThisSheet.href !== null && ThisSheet.href.includes(location.hostname))
{
var Rules = ThisSheet[cssRuleCode];
for(var RuleIndex = Rules.length - 1; RuleIndex >= 0; --RuleIndex)
{
var ThisRule = Rules[RuleIndex];
if(SelectorText === ThisRule.selectorText)
{
Result = ThisRule;
break;
}
}
if(Result !== undefined) { break; }
}
}
return Result;
}
function
GetRulesOfStyleSheetIndex(Index)
{
var Result = undefined;
var StyleSheets = document.styleSheets;
var cssRuleCode = document.all ? 'rules' : 'cssRules'; // account for IE and FF
var StyleSheet = StyleSheets[Index];
if(StyleSheet)
{
Result = StyleSheet[cssRuleCode];
}
return Result;
}
function
GetLocalStyleSheet()
{
var Result;
var StyleSheets = document.styleSheets;
for(var StyleSheetIndex = StyleSheets.length - 1; StyleSheetIndex >= 0; --StyleSheetIndex)
{
var This = StyleSheets[StyleSheetIndex];
if(This.href !== null && This.href.includes(location.hostname) && !This.disabled)
{
Result = This;
break;
}
}
return Result;
}
function
GetOrSetRule(SelectorText)
{
var Result = GetRule(SelectorText);
if(Result === undefined)
{
var StyleSheet = GetLocalStyleSheet();
if(StyleSheet)
{
var cssRuleCode = document.all ? 'rules' : 'cssRules'; // account for IE and FF
var Rules = StyleSheet[cssRuleCode];
var RuleIndex = StyleSheet.insertRule(SelectorText + "{}", Rules.length - 1);
Result = Rules[RuleIndex];
}
}
return Result;
}
function IsInRangeEx(Min, N, Max)
{
return N > Min && N < Max;
}
/* Auto-scrolling */
var ScrollTriggeredInternally = false;
var LastScrollYPos = 0;
var ScrollTicking = false;
var ScrollerFunction;
var ScrollCondition;
function ScrollTo(Element, ScrollPos, IsMobile, StickyObscuringElement) {
if(Element.offsetWidth && Element.offsetHeight)
{
var ScrolledToTop = ScrollPos == 0;
var ScrolledToBottom = (window.innerHeight + Math.ceil(window.pageYOffset)) >= document.body.scrollHeight;
if(!ScrolledToTop || !ScrolledToBottom)
{
var Ceiling = StickyObscuringElement ? StickyObscuringElement.offsetHeight : 0;
var VisibleArea = GetWindowDim(IsMobile);
VisibleArea.Y -= Ceiling;
var PercentageOfVisibleHeightToGather = 5;
var GatherableHeight = VisibleArea.Y * PercentageOfVisibleHeightToGather / 100;
var BoundingRect = Element.getBoundingClientRect();
var ElementTop = BoundingRect.top - Ceiling;
var ElementBottom = ElementTop + BoundingRect.height;
var UpperProtrusion = -ElementTop;
var LowerProtrusion = ElementBottom - VisibleArea.Y;
var DesiredScroll = null;
var YOffsetFromPage = getElementYOffsetFromPage(Element);
if(IsInRangeEx(0, UpperProtrusion, GatherableHeight))
{
if(!ScrolledToBottom)
{
DesiredScroll = YOffsetFromPage - Ceiling;
}
}
else if(IsInRangeEx(0, LowerProtrusion, GatherableHeight))
{
if(!ScrolledToTop)
{
if(IsInRangeEx(0, UpperProtrusion + LowerProtrusion, GatherableHeight))
{
DesiredScroll = YOffsetFromPage - Ceiling;
}
else
{
DesiredScroll = ScrollPos + LowerProtrusion;
}
}
}
if(DesiredScroll !== null && DesiredScroll != ScrollPos)
{
window.scrollTo({
top: DesiredScroll,
behavior: "smooth"
});
}
}
}
}
function
InitScrollEventListener(Element, IsMobile, StickyObscuringElement)
{
window.addEventListener('scroll', function() {
if(ScrollTriggeredInternally)
{
ScrollTriggeredInternally = false;
}
else if(ScrollCondition == undefined || ScrollCondition == true)
{
LastScrollYPos = window.scrollY;
if (!ScrollTicking) {
window.requestAnimationFrame(function() {
clearTimeout(ScrollerFunction);
ScrollerFunction = setTimeout(ScrollTo, 2000, Element, LastScrollYPos, IsMobile, StickyObscuringElement);
ScrollTicking = false;
});
ScrollTicking = true;
}
}
});
}
/* /Auto-scrolling */
function getElementXOffsetFromPage(el) {
var left = 0;
do {
left += el.offsetLeft;
} while (el = el.offsetParent);
return left;
}
function getElementYOffsetFromPage(el) {
var top = 0;
do {
top += el.offsetTop;
} while (el = el.offsetParent);
return top;
}
function
MaxWidthOfElement(Element, WindowDim)
{
// NOTE(matt): This works fine for Elements whose natural max width fills the whole line, i.e. block elements and children
// thereof. To support inline elements, we'll need to mirror MaxHeightOfElement() and may as well roll the two
// functions into one.
var Result = 0;
var OriginalWidth = Element.style.width;
Element.style.width = "100%";
var NaturalMax = Element.offsetWidth;
Element.style.width = OriginalWidth;
var InnerWidth = WindowDim ? WindowDim.X : document.documentElement.clientWidth;
Result = Math.min(NaturalMax, InnerWidth);
return Result;
}
function
MaxHeightOfElement(Element, WindowDim)
{
var Result = 0;
var DisplaySettings = [];
for(var i = 0; i < Element.children.length; ++i)
{
var Child = Element.children[i];
DisplaySettings.push(Child.style.display);
Child.style.display = "none";
}
var OriginalHeight = Element.style.height;
Element.style.height = "100%";
var NaturalMax = Element.offsetHeight;
Element.style.height = OriginalHeight;
var InnerHeight = WindowDim ? WindowDim.Y : document.documentElement.clientHeight;
if(NaturalMax > 0)
{
Result = Math.min(NaturalMax, InnerHeight);
}
else
{
Result = InnerHeight;
}
for(var i = 0; i < Element.children.length; ++i)
{
var Child = Element.children[i];
Child.style.display = DisplaySettings.shift();
}
return Result;
}
function
MaxDimensionsOfElement(Element, WindowDim)
{
var Result = {
X: null,
Y: null,
};
Result.X = MaxWidthOfElement(Element, WindowDim);
Result.Y = MaxHeightOfElement(Element, WindowDim);
return Result;
}
function
Clamp(EndA, N, EndB)
{
var Min = EndA < EndB ? EndA : EndB;
var Max = EndA > EndB ? EndA : EndB;
return N < Min ? Min : N > Max ? Max : N;
}
function
Clamp01(N)
{
return N < 0 ? 0 : N > 1 ? 1 : N;
}
function
Lerp(A, t, B)
{
return (1-t)*A + t*B;
}
function
IsOverflowed(Element)
{
return Element.scrollHeight > Element.clientHeight || Element.scrollWidth > Element.clientWidth;
}
function
SetHelpUnfocused(Button)
{
Button.firstElementChild.innerText = "¿";
Button.firstElementChild.title = "Keypresses will not pass through to Cinera because focus is currently elsewhere.\n\nTo regain focus, please press Tab / Shift-Tab (multiple times) or click somewhere related to Cinera other than the video, e.g. this button";
}
function
SetHelpFocused(Button)
{
Button.firstElementChild.innerText = "?";
Button.firstElementChild.title = ""
}
function
BindHelp(Button, DocumentationContainer)
{
window.addEventListener("blur", function(){
SetHelpUnfocused(Button);
});
window.addEventListener("focus", function(){
SetHelpFocused(Button);
});
Button.addEventListener("click", function() {
DocumentationContainer.classList.toggle("visible");
})
}
function getBackgroundColourRGB(element) {
var Colour = getComputedStyle(element).getPropertyValue("background-color");
var depth = 0;
while((Colour == "transparent" || Colour == "rgba(0, 0, 0, 0)") && element.parentElement && depth <= 4)
{
element = element.parentElement;
Colour = getComputedStyle(element).getPropertyValue("background-color");
++depth;
}
var Staging = Colour.slice(Colour.indexOf("(") + 1, -1).split(", ");
var Result = {
R: parseInt(Staging[0]),
G: parseInt(Staging[1]),
B: parseInt(Staging[2]),
};
return Result;
}
function getBackgroundBrightness(element) {
var Colour = getBackgroundColourRGB(element);
var Result = Math.sqrt(Colour.R * Colour.R * .241 +
Colour.G * Colour.G * .691 +
Colour.B * Colour.B * .068);
return Result;
}
function setTextLightness(textElement)
{
var textHue = textElement.getAttribute("data-hue");
var textSaturation = textElement.getAttribute("data-saturation");
if(textHue && textSaturation)
{
if(getBackgroundBrightness(textElement.parentNode) < 127)
{
textElement.style.color = ("hsl(" + textHue + ", " + textSaturation + ", 76%)");
}
else
{
textElement.style.color = ("hsl(" + textHue + ", " + textSaturation + ", 24%)");
}
}
}
function setDotLightness(topicDot)
{
var dotHue = topicDot.getAttribute("data-hue");
var dotSaturation = topicDot.getAttribute("data-saturation");
if(dotHue && dotSaturation)
{
if(getBackgroundBrightness(topicDot.parentNode) < 127)
{
topicDot.style.backgroundColor = ("hsl(" + dotHue + ", " + dotSaturation + ", 76%)");
topicDot.style.borderColor = ("hsl(" + dotHue + ", " + dotSaturation + ", 76%)");
}
else
{
topicDot.style.backgroundColor = ("hsl(" + dotHue + ", " + dotSaturation + ", 47%)");
topicDot.style.borderColor = ("hsl(" + dotHue + ", " + dotSaturation + ", 47%)");
}
}
}

View File

@ -1,248 +0,0 @@
if (location.hash && location.hash.length > 0) {
var initialQuery = location.hash;
if (initialQuery[0] == "#") {
initialQuery = initialQuery.slice(1);
}
document.getElementById("query").value = decodeURIComponent(initialQuery);
}
var indexContainer = document.getElementById("cineraIndex");
var lastQuery = null;
var resultsToRender = [];
var resultsIndex = 0;
var resultsMarkerIndex = 0;
var resultsContainer = document.getElementById("cineraResults");
var rendering = false;
var dayContainerPrototype = document.createElement("DIV");
dayContainerPrototype.classList.add("dayContainer");
dayContainerPrototype.classList.add(theme);
var dayNamePrototype = document.createElement("SPAN");
dayNamePrototype.classList.add("dayName");
dayContainerPrototype.appendChild(dayNamePrototype);
var markerListPrototype = document.createElement("DIV");
markerListPrototype.classList.add("markerList");
markerListPrototype.classList.add(theme);
dayContainerPrototype.appendChild(markerListPrototype);
var markerPrototype = document.createElement("A");
markerPrototype.classList.add("marker");
markerPrototype.setAttribute("target", "_blank");
var highlightPrototype = document.createElement("B");
var episodes = [];
function getEpisodeName(filename) {
var day = filename;
var dayParts = day.match(/([a-zA-Z_-]+)([0-9]+)?([a-zA-Z]+)?/);
day = dayParts[1].slice(0, 1).toUpperCase() + dayParts[1].slice(1) + (dayParts[2] ? " " + dayParts[2] : "") + (dayParts[3] ? " " + dayParts[3].toUpperCase() : "");
return day;
}
function markerTime(totalTime) {
var markTime = "(";
var hours = Math.floor(totalTime / 60 / 60);
var minutes = Math.floor(totalTime / 60) % 60;
var seconds = totalTime % 60;
if (hours > 0) {
markTime += padTimeComponent(hours) + ":";
}
markTime += padTimeComponent(minutes) + ":" + padTimeComponent(seconds) + ")";
return markTime;
}
function padTimeComponent(component) {
return (component < 10 ? "0" + component : component);
}
function runSearch() {
var queryStr = document.getElementById("query").value;
if (lastQuery != queryStr) {
var oldResultsContainer = resultsContainer;
resultsContainer = oldResultsContainer.cloneNode(false);
oldResultsContainer.parentNode.insertBefore(resultsContainer, oldResultsContainer);
oldResultsContainer.remove();
resultsIndex = 0;
resultsMarkerIndex = 0;
}
lastQuery = queryStr;
resultsToRender = [];
var numEpisodes = 0;
var numMarkers = 0;
var totalSeconds = 0;
if (queryStr && queryStr.length > 0) {
indexContainer.style.display = "none";
if (episodes.length > 0) {
var query = new RegExp(queryStr.replace("(", "\\(").replace(")", "\\)").replace(/(^|[^\\])\\$/, "$1"), "gi");
for (var i = 0; i < episodes.length; ++i) {
var episode = episodes[i];
var matches = [];
for (var j = 0; j < episode.markers.length; ++j) {
query.lastIndex = 0;
var result = query.exec(episode.markers[j].text);
if (result && result[0].length > 0) {
numMarkers++;
matches.push(episode.markers[j]);
if (j < episode.markers.length-1) {
totalSeconds += episode.markers[j+1].totalTime - episode.markers[j].totalTime;
}
}
}
if (matches.length > 0) {
numEpisodes++;
resultsToRender.push({
query: query,
episode: episode,
matches: matches
});
}
}
if (!rendering) {
renderResults();
}
} else {
document.querySelector(".spinner").classList.add("show");
}
}
else
{
indexContainer.style.display = "block";
}
var totalTime = Math.floor(totalSeconds/60/60) + "h " + Math.floor(totalSeconds/60)%60 + "m " + totalSeconds%60 + "s ";
document.getElementById("cineraResultsSummary").textContent = "Found: " + numEpisodes + " episodes, " + numMarkers + " markers, " + totalTime + "total.";
}
function renderMatches(renderStart) {
var query = resultsToRender[resultsIndex].query;
var episode = resultsToRender[resultsIndex].episode;
var matches = resultsToRender[resultsIndex].matches;
var markerList = null;
if (resultsMarkerIndex == 0) {
var dayContainer = dayContainerPrototype.cloneNode(true);
var dayName = dayContainer.children[0];
markerList = dayContainer.children[1];
dayName.textContent = episode.day + ": " + episode.title;
resultsContainer.appendChild(dayContainer);
} else {
markerList = document.querySelector("#cineraResults > .dayContainer:nth-child(" + (resultsIndex+1) + ") .markerList");
}
do {
var match = matches[resultsMarkerIndex];
var marker = markerPrototype.cloneNode();
var playerURLPrefix = (baseURL ? baseURL + "/" : "") + (playerLocation ? playerLocation + "/" : "");
marker.setAttribute("href", playerURLPrefix + episode.filename.replace(/"/g, "") + "/#" + match.totalTime);
query.lastIndex = 0;
var cursor = 0;
var text = match.text;
var result = null;
marker.appendChild(document.createTextNode(match.prettyTime + " "));
while (result = query.exec(text)) {
if (result.index > cursor) {
marker.appendChild(document.createTextNode(text.slice(cursor, result.index)));
}
var highlightEl = highlightPrototype.cloneNode();
highlightEl.textContent = result[0];
marker.appendChild(highlightEl);
cursor = result.index + result[0].length;
}
if (cursor < text.length) {
marker.appendChild(document.createTextNode(text.slice(cursor, text.length)));
}
markerList.appendChild(marker);
resultsMarkerIndex++;
} while (resultsMarkerIndex < matches.length && performance.now() - renderStart < 1);
return resultsMarkerIndex == matches.length;
}
function renderResults() {
if (resultsIndex < resultsToRender.length) {
rendering = true;
var renderStart = performance.now();
while (resultsIndex < resultsToRender.length && performance.now() - renderStart < 1) {
var done = renderMatches(renderStart);
if (done) {
resultsMarkerIndex = 0;
resultsIndex++;
}
}
requestAnimationFrame(renderResults);
} else {
rendering = false;
}
}
var queryEl = document.getElementById("query")
queryEl.addEventListener("input", function(ev) {
history.replaceState(null, null, "#" + encodeURIComponent(queryEl.value));
runSearch();
});
var xhr = new XMLHttpRequest();
xhr.addEventListener("load", function() {
var contents = xhr.response;
var lines = contents.split("\n");
var mode = "none";
var episode = null;
for (var i = 0; i < lines.length; ++i) {
var line = lines[i];
if (line.trim().length == 0) { continue; }
if (line == "---") {
if (episode != null) {
episode.filename = episode.name;
episode.day = getEpisodeName(episode.filename + ".html.md");
episodes.push(episode);
}
episode = {};
mode = "none";
} else if (line.startsWith("name:")) {
if(projectID != outputURLPrefix) {
episode.name = line.slice(6).replace(projectID, outputURLPrefix);
}
else {
episode.name = line.slice(6);
}
} else if (line.startsWith("title:")) {
episode.title = line.slice(7).replace(/"/g, "");
} else if (line.startsWith("markers")) {
mode = "markers";
episode.markers = [];
} else if (mode == "markers") {
var match = line.match(/"(\d+)": "(.+)"/);
if (match == null) {
console.log(name, line);
} else {
var totalTime = parseInt(line.slice(1));
var marker = {
totalTime: totalTime,
prettyTime: markerTime(totalTime),
text: match[2].replace(/\\"/g, "\"")
}
episode.markers.push(marker);
}
}
}
document.querySelector(".spinner").classList.remove("show");
runSearch();
});
xhr.addEventListener("error", function() {
console.error("Failed to load content");
});
var indexLocation = (baseURL ? baseURL + "/" : "") + projectID + ".index";
xhr.open("GET", indexLocation);
xhr.setRequestHeader("Content-Type", "text/plain");
xhr.send();
runSearch();

View File

@ -0,0 +1,67 @@
document.body.style.overflowY = "scroll";
CineraProps.Orientation = GetRealOrientation(orientations.LANDSCAPE_LEFT, CineraProps.IsMobile);
// Element Selection
//
Nav.Nexus = document.getElementById("cineraIndex");
Nav.Controls.Header = document.getElementById("cineraIndexControl");
Nav.Controls.Sort = Nav.Controls.Header.querySelector(".cineraMenuItem.sort");
Nav.Controls.View = Nav.Controls.Header.querySelector(".cineraMenuItem.view");
Nav.Controls.Anim = Nav.Controls.Header.querySelector(".cineraMenuItem.anim");
Nav.Controls.Save = Nav.Controls.Header.querySelector(".cineraMenuItem.save");
Nav.Controls.Help = Nav.Nexus.querySelector(".cineraHelp");
Nav.Controls.HelpDocumentation = Nav.Controls.Help.querySelector(".help_container");
Nav.GridContainer = Nav.Nexus.querySelector(".cineraIndexGridContainer");
Nav.Controls.GridTraversal.Container = Nav.GridContainer.querySelector(".cineraTraversalContainer");
Nav.Controls.GridTraversal.Header = Nav.GridContainer.querySelector(".cineraTraversal");
Nav.Controls.GridTraversal.Ascend = Nav.Controls.GridTraversal.Header.querySelector(".cineraButton.ascension");
Nav.Controls.GridTraversal.Prev = Nav.Controls.GridTraversal.Header.querySelector(".cineraButton.prev");
Nav.Controls.GridTraversal.Next = Nav.Controls.GridTraversal.Header.querySelector(".cineraButton.next");
Search.QueryElement = document.getElementById("query");
Search.ResultsSummary = document.getElementById("cineraResultsSummary");
Search.ResultsContainer = document.getElementById("cineraResults");
Search.IndexContainer = document.getElementById("cineraIndexList");
Search.ProjectsContainer = Search.IndexContainer.querySelectorAll(".cineraIndexProject");
//
///
// NOTE(matt): Initialisation
//
if(CineraProps.IsMobile)
{
Nav.Nexus.classList.add("mobile");
}
InitTraversalStack();
InitNexus();
InitHelpKeys(Nav.Controls.HelpDocumentation);
Nav.GridSize = ComputeOptimalGridSize();
SetHelpKeyAvailability(Nav.GridSize);
InitButtons(); // NOTE(matt): Also does "keydown" listeners, needed before UpdateButtons()
UpdateButtons();
InitQuery(Search.QueryElement);
InitPrototypes(Search.ResultsContainer);
prepareProjects();
SyncNavState();
FlipClear("scroll");
//
////
// NOTE(matt): Listeners
//
BindControlKeys();
BindGridKeys(Nav.GridSize);
BindControls();
InitResizeEventListener();
InitOrientationChangeListener();
InitScrollEventListener(Nav.GridContainer, CineraProps.IsMobile, Nav.Controls.Header);
//
////
// NOTE(matt): On-load Execution
//
runSearch();
//
////

3882
cinera/cinera_search_pre.js Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 881 B

After

Width:  |  Height:  |  Size: 893 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 855 B

After

Width:  |  Height:  |  Size: 801 B

View File

@ -8,8 +8,10 @@
<body>
<a href="../">Awesome Contents</a>
<h1 style="background-color: #3399FF; text-align:center; width: 100%; margin: 0 auto"><!-- __CINERA_TITLE__ --></h1>
<!-- __CINERA_MENUS__ -->
<!-- __CINERA_PLAYER__ -->
<div>
<!-- __CINERA_MENUS__ -->
<!-- __CINERA_PLAYER__ -->
</div>
<!-- 2. This is a random comment. How does this affect it, if at all? -->
<!-- __CINERA_SCRIPT__ -->
<!-- 3. This is a random comment. How does this affect it, if at all? -->

7656
cinera/stb_image.h Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -1,98 +0,0 @@
#ifndef HMML_H_
#define HMML_H_
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <stdio.h>
// Data structures
typedef struct {
char* member;
char* stream_platform;
char* stream_username;
char* project;
char* title;
char* vod_platform;
char* id;
char** co_hosts;
size_t co_host_count;
char** guests;
size_t guest_count;
char** annotators;
size_t annotator_count;
} HMML_VideoMetaData;
typedef struct {
char* site;
char* page;
char* url;
char* title;
char* article;
char* author;
char* editor;
char* publisher;
char* isbn;
int offset;
} HMML_Reference;
typedef enum {
HMML_CATEGORY,
HMML_MEMBER,
HMML_PROJECT,
HMML_MARKER_COUNT,
} HMML_MarkerType;
typedef struct {
HMML_MarkerType type;
char* marker;
char* parameter;
char* episode;
int offset;
} HMML_Marker;
typedef struct {
int id;
char* author;
} HMML_Quote;
typedef struct {
int line;
char* time;
char* text;
char* author;
HMML_Reference* references;
size_t reference_count;
HMML_Marker* markers;
size_t marker_count;
HMML_Quote quote;
bool is_quote;
} HMML_Annotation;
typedef struct {
int line;
char* message;
} HMML_Error;
typedef struct {
bool well_formed;
HMML_VideoMetaData metadata;
HMML_Annotation* annotations;
size_t annotation_count;
HMML_Error error;
} HMML_Output;
// Functions
HMML_Output hmml_parse_file (FILE* file);
void hmml_dump (HMML_Output* output);
void hmml_free (HMML_Output* output);
#endif

View File

@ -1,12 +1,12 @@
#ifndef HMML_H_
#define HMML_H_
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <stdio.h>
// Data structures
#define HMML_CUSTOM_ATTR_COUNT 16
typedef struct {
char* member;
char* stream_platform;
@ -28,6 +28,11 @@ typedef struct {
char* template;
char* medium;
char* custom[HMML_CUSTOM_ATTR_COUNT];
long publication_date;
char* output;
} HMML_VideoMetaData;
typedef struct {
@ -69,7 +74,7 @@ typedef struct {
char* time;
char* text;
char* author;
HMML_Reference* references;
size_t reference_count;
@ -77,7 +82,7 @@ typedef struct {
size_t marker_count;
HMML_Quote quote;
bool is_quote;
_Bool is_quote;
} HMML_Annotation;
typedef struct {
@ -86,7 +91,7 @@ typedef struct {
} HMML_Error;
typedef struct {
bool well_formed;
_Bool well_formed;
HMML_VideoMetaData metadata;
HMML_Annotation* annotations;
size_t annotation_count;

View File

@ -5,7 +5,7 @@
#include "stb_sb.h"
#include "hmmlib.h"
const struct HMML_Version hmml_version = { 0, 2, 0 };
const struct HMML_Version hmml_version = { 0, 4, 0 };
typedef struct {
int line;
@ -58,10 +58,10 @@
yyextra->first = false;\
} while(0)
#define UNQUOTE(_attr) ({ \
#define UNQUOTE_LEN(_attr, _len) ({ \
typeof(_attr) attr = (_attr); \
size_t len = strlen(attr); \
for(char* c = attr; *c; ++c){\
typeof(_len) len = (_len); \
for(char* c = attr; c < attr+len; ++c){\
if(*c == '\\'){ \
CHECKESCAPE(c[1]); \
memmove(c, c+1, len-(c-attr)); \
@ -71,6 +71,8 @@
attr; \
})
#define UNQUOTE(_attr) UNQUOTE_LEN(_attr, strlen(attr))
%}
%option reentrant
@ -80,7 +82,7 @@
S [\t \r]*
ATTR_SIMPLE [^\" \]\t\r\n][^ \]\t\r\n]*
ATTR_ALNUM [0-9a-zA-Z][0-9a-zA-Z_]*
ATTR_QUOTED \"([^\n\"\\]|\\.)*\"
ATTR_QUOTED \"([^\"\\]|\\.)*\"
TAG_VIDEO_OPEN \[video
TIMECODE \[[0-9]{1,2}(:[0-5][0-9]){1,2}\]
BAD_TIMECODE \[[0-9]{1,2}(:[6-9][0-9]){1,2}\]
@ -91,6 +93,7 @@ RB \]
%s VIDEO
%s V_ATTR
%s V2_ATTR
%s V3_ATTR
%s ANNOTATION
%s TEXT_START
%s TEXT
@ -121,11 +124,29 @@ RB \]
<VIDEO>id{S}= { yyextra->attr = V_(id); BEGIN(V_ATTR); }
<VIDEO>template{S}= { yyextra->attr = V_(template); BEGIN(V_ATTR); }
<VIDEO>medium{S}= { yyextra->attr = V_(medium); BEGIN(V_ATTR); }
<VIDEO>custom0{S}= { yyextra->attr = V_(custom[0]); BEGIN(V_ATTR); } // probably smarter way to do this ¯\_(ツ)_/¯
<VIDEO>custom1{S}= { yyextra->attr = V_(custom[1]); BEGIN(V_ATTR); }
<VIDEO>custom2{S}= { yyextra->attr = V_(custom[2]); BEGIN(V_ATTR); }
<VIDEO>custom3{S}= { yyextra->attr = V_(custom[3]); BEGIN(V_ATTR); }
<VIDEO>custom4{S}= { yyextra->attr = V_(custom[4]); BEGIN(V_ATTR); }
<VIDEO>custom5{S}= { yyextra->attr = V_(custom[5]); BEGIN(V_ATTR); }
<VIDEO>custom6{S}= { yyextra->attr = V_(custom[6]); BEGIN(V_ATTR); }
<VIDEO>custom7{S}= { yyextra->attr = V_(custom[7]); BEGIN(V_ATTR); }
<VIDEO>custom8{S}= { yyextra->attr = V_(custom[8]); BEGIN(V_ATTR); }
<VIDEO>custom9{S}= { yyextra->attr = V_(custom[9]); BEGIN(V_ATTR); }
<VIDEO>custom10{S}= { yyextra->attr = V_(custom[10]); BEGIN(V_ATTR); }
<VIDEO>custom11{S}= { yyextra->attr = V_(custom[11]); BEGIN(V_ATTR); }
<VIDEO>custom12{S}= { yyextra->attr = V_(custom[12]); BEGIN(V_ATTR); }
<VIDEO>custom13{S}= { yyextra->attr = V_(custom[13]); BEGIN(V_ATTR); }
<VIDEO>custom14{S}= { yyextra->attr = V_(custom[14]); BEGIN(V_ATTR); }
<VIDEO>custom15{S}= { yyextra->attr = V_(custom[15]); BEGIN(V_ATTR); }
<VIDEO>co\-host{S}= { yyextra->attr = V_(co_hosts); BEGIN(V2_ATTR); }
<VIDEO>guest{S}= { yyextra->attr = V_(guests); BEGIN(V2_ATTR); }
<VIDEO>annotator{S}= { yyextra->attr = V_(annotators); BEGIN(V2_ATTR); }
<VIDEO>publication_date{S}= { yyextra->attr = V_(publication_date); BEGIN(V3_ATTR); }
<VIDEO>output{S}= { yyextra->attr = V_(output); BEGIN(V_ATTR); }
<VIDEO>\] { BEGIN(ANNOTATION); };
<VIDEO>. { HMML_ERR("Invalid char '%s' in video tag.", yytext); }
<VIDEO>. { HMML_ERR("Unknown attribute in video tag, beginning with '%s'.", yytext); }
<V_ATTR>{S} { BEGIN(VIDEO); }
<V_ATTR>{ATTR_SIMPLE} { *(char**)yyextra->attr = strndup(yytext , yyleng ); BEGIN(VIDEO); }
@ -137,6 +158,11 @@ RB \]
<V2_ATTR>{ATTR_QUOTED} { sb_push(*(char***)yyextra->attr, UNQUOTE(strndup(yytext+1, yyleng-2))); BEGIN(VIDEO); }
<V2_ATTR>\] { yyless(0); BEGIN(VIDEO); }
<V3_ATTR>{S} { BEGIN(VIDEO); }
<V3_ATTR>{ATTR_SIMPLE} { *(long*)yyextra->attr = atol(yytext); BEGIN(VIDEO); }
<V3_ATTR>{ATTR_QUOTED} { *(long*)yyextra->attr = atol(UNQUOTE_LEN(yytext+1, yyleng-2)); BEGIN(VIDEO); }
<V3_ATTR>\] { yyless(0); BEGIN(VIDEO); }
<ANNOTATION>{TIMECODE}{LB}@ { NEWANNO(); yyextra->an.time = strndup(yytext+1, yyleng-4); BEGIN(AUTHOR); }
<ANNOTATION>{TIMECODE} { NEWANNO(); yyextra->an.time = strndup(yytext+1, yyleng-2); BEGIN(TEXT_START); }
<ANNOTATION>{BAD_TIMECODE} { HMML_ERR("Timecode %s out of range.", yytext); }
@ -324,6 +350,11 @@ void hmml_free(HMML_Output* hmml){
free(hmml->metadata.id);
free(hmml->metadata.template);
free(hmml->metadata.medium);
free(hmml->metadata.output);
for(size_t i = 0; i < HMML_CUSTOM_ATTR_COUNT; ++i){
free(hmml->metadata.custom[i]);
}
sb_each(i, hmml->metadata.co_hosts) free(*i);
sb_each(i, hmml->metadata.guests) free(*i);
@ -363,7 +394,6 @@ void hmml_dump(HMML_Output* hmml){
return;
}
puts("Annotations:");
for(size_t i = 0; i < hmml->annotation_count; ++i){
HMML_Annotation* a = hmml->annotations + i;

View File

@ -3,8 +3,16 @@ all: hmml.a example
hmml.a: hmmlib.o
ar rcs $@ $<
hmmlib.c: hmmlib.l
lex -o $@ $<
hmmlib.o: hmmlib.c
gcc -D_GNU_SOURCE -g -c $< -o $@
gcc -std=gnu99 -D_GNU_SOURCE -g -c $< -o $@
example: example.c hmml.a
gcc -g $^ -o $@
gcc -std=gnu99 -g $^ -o $@
clean:
$(RM) hmml.a hmmlib.o hmmlib.c example
.PHONY: all clean

5
hmmlib2/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/utils/hmmldump
/utils/fuzz/fuzz.gcno
/utils/fuzz/fuzz.gcda
/utils/fuzz/output
/utils/fuzz/fuzz

6
hmmlib2/README.TXT Normal file
View File

@ -0,0 +1,6 @@
This is a single-header library for parsing the HMML Format.
See https://git.handmade.network/Annotation-Pushers/Annotation-System/-/wikis/hmmlspec
To use:
1. #include "hmmlib.h" in any files that use the functions / data structures.
2. in **one** of your .c files, #define HMMLIB_IMPLEMENTATION before including it.

859
hmmlib2/hmmlib.h Normal file
View File

@ -0,0 +1,859 @@
#ifndef HMMLIB_H_
#define HMMLIB_H_
#include <stddef.h>
#include <stdio.h>
// Data structures
typedef struct {
char* name;
char* role;
} HMML_Credit;
typedef struct {
char* key;
char* value;
} HMML_VideoCustomMetaData;
typedef struct {
char* stream_platform;
char* project;
char* title;
char* vod_platform;
char* id;
char* output;
char* template;
char* medium;
char* number;
char* cc_lang;
HMML_Credit* credits;
size_t credit_count;
HMML_Credit* uncredits;
size_t uncredit_count;
HMML_VideoCustomMetaData* custom;
size_t custom_count;
} HMML_VideoMetaData;
typedef struct {
char* site;
char* page;
char* url;
char* title;
char* article;
char* author;
char* editor;
char* publisher;
char* isbn;
int offset;
} HMML_Reference;
typedef enum {
HMML_CATEGORY,
HMML_MEMBER,
HMML_PROJECT,
HMML_MARKER_COUNT,
} HMML_MarkerType;
typedef struct {
HMML_MarkerType type;
char* marker;
char* parameter;
char* episode;
int offset;
} HMML_Marker;
typedef struct {
_Bool present;
int id;
char* author;
} HMML_Quote;
typedef struct {
int line;
int h, m, s, ms;
char* text;
char* author;
HMML_Reference* references;
size_t reference_count;
HMML_Marker* markers;
size_t marker_count;
HMML_Quote quote;
} HMML_Timestamp;
typedef struct {
int line;
int col;
char* message;
} HMML_Error;
typedef struct {
_Bool well_formed;
HMML_VideoMetaData metadata;
HMML_Timestamp* timestamps;
size_t timestamp_count;
HMML_Error error;
void* free_list; // implementation detail
} HMML_Output;
// Functions
HMML_Output hmml_parse (const char* string);
void hmml_free (HMML_Output* output);
// Version
extern const struct HMML_Version {
int Major, Minor, Patch;
} hmml_version;
#endif
#ifdef HMMLIB_IMPLEMENTATION
#include <setjmp.h>
#include <string.h>
#include <stdarg.h>
#include <stdlib.h>
#include <stdint.h>
#define HSTX(x) x, sizeof(x)-1
#define HSTR(x) (const struct _hmml_str){ HSTX(x) }
#ifndef MALLOC
#define MALLOC malloc
#endif
#ifndef REALLOC
#define REALLOC realloc
#endif
#ifndef countof
#define countof(x) (sizeof(x)/sizeof(*x))
#endif
#define _hmml_debug(...)
//#define _hmml_debug printf
struct _hmml_parser {
HMML_Output out;
const char* mem;
const char* cursor;
jmp_buf err_buf;
uintptr_t* free_list;
int line;
};
struct _hmml_str {
const char* ptr;
size_t len;
};
// memory management boilerplate stuff
static void* _hmml_store_ptr(struct _hmml_parser* p, void* input)
{
uintptr_t* ptr;
if(p->free_list) {
ptr = p->free_list;
if(ptr[1] + 1 == ptr[0]) {
size_t n = ptr[0] << 1;
ptr = REALLOC(ptr, n * sizeof(uintptr_t));
ptr[0] = n;
}
ptr[ptr[1]] = (uintptr_t)input;
ptr[1]++;
} else {
ptr = MALLOC(8 * sizeof(uintptr_t));
ptr[0] = 8;
ptr[1] = 3;
ptr[2] = (uintptr_t)input;
}
p->free_list = ptr;
return input;
}
static char* _hmml_persist_str(struct _hmml_parser* p, const struct _hmml_str str)
{
char* mem = MALLOC(str.len+1);
memcpy(mem, str.ptr, str.len);
mem[str.len] = '\0';
return _hmml_store_ptr(p, mem);
}
static void _hmml_persist_array_fn(struct _hmml_parser* p, void** out, size_t* out_count, void* in, size_t in_size)
{
void* base;
if(!*out) {
base = MALLOC(in_size + sizeof(size_t));
_hmml_store_ptr(p, base);
*(size_t*)base = p->free_list[1]-1;
} else {
base = (char*)(*out) - sizeof(size_t);
base = REALLOC(base, (*out_count + 1) * in_size + sizeof(size_t));
size_t free_list_off = *(size_t*)base;
p->free_list[free_list_off] = (intptr_t)base;
}
*out = (char*)base + sizeof(size_t);
memcpy((char*)*out + (*out_count * in_size), in, in_size);
++(*out_count);
}
#define _hmml_persist_array(p, out, out_count, in) \
_hmml_persist_array_fn((p), (void**)(out), (out_count), &(in), sizeof(in))
// error handling
#define _hmml_err(p, fmt, ...) \
_hmml_err_fn((p), fmt "\n", ##__VA_ARGS__)
__attribute__((noreturn))
static void _hmml_err_fn(struct _hmml_parser* p, const char* fmt, ...)
{
static char error_buf[4096];
va_list va;
va_start(va, fmt);
int n = vsnprintf(error_buf, sizeof(error_buf), fmt, va);
va_end(va);
int line = 1, col = 1;
for(const char* ptr = p->mem; ptr != p->cursor; ++ptr) {
if(*ptr == '\n') {
++line;
col = 1;
} else {
++col;
}
}
p->out.error.message = _hmml_persist_str(p, (struct _hmml_str){ error_buf, n });
p->out.error.line = line;
p->out.error.col = col;
longjmp(p->err_buf, 1);
}
// actual parsing stuff
static void _hmml_skip_ws(struct _hmml_parser* p)
{
for(;;) {
uint8_t c = *p->cursor;
if(c && c <= ' ') {
if(c == '\n') {
++p->line;
}
++p->cursor;
} else {
break;
}
}
}
static _Bool _hmml_str_eq(struct _hmml_str a, struct _hmml_str b)
{
return a.len == b.len && memcmp(a.ptr, b.ptr, a.len) == 0;
}
static _Bool _hmml_unesc(char in, char* out)
{
if(strchr("[]:@~\\\"", in)) {
*out = in;
return 1;
} else {
return 0;
}
}
static char* _hmml_read_attr(struct _hmml_parser* p, char* mem, size_t mem_size, _Bool break_on_punct)
{
const char* src = p->cursor;
char* dst = mem;
if(*src == '"') {
++src;
while(*src && *src != '"' && (size_t)(src - p->cursor) < mem_size) {
char converted;
if(*src == '\\' && _hmml_unesc(src[1], &converted)) {
*dst++ = converted;
src += 2;
} else {
*dst++ = *src++;
}
}
if(*src != '"') {
_hmml_err(p, "Partially quoted attribute");
}
*dst = '\0';
p->cursor = src+1;
} else {
const char* breaks = break_on_punct
? " ]\r\n\t:,'-.#=[\\?!…()\"%"
: " ]\r\n\t"
;
size_t n = strcspn(src, breaks);
if(n >= mem_size) {
_hmml_err(p, "Attribute [%.10s...] too long", p->cursor);
}
memcpy(dst, src, n);
dst += n;
*dst = '\0';
p->cursor += n;
}
return dst;
}
static void _hmml_read_kv(struct _hmml_parser* p, struct _hmml_str* key, struct _hmml_str* val)
{
static char key_memory[64];
static char val_memory[1024];
size_t key_len = strcspn(p->cursor, " \r\n\t=");
if(key_len >= sizeof(key_memory)) {
_hmml_err(p, "Attribute key [%.10s...] too long", p->cursor);
}
memcpy(key_memory, p->cursor, key_len);
key_memory[key_len] = '\0';
p->cursor += key_len;
_hmml_skip_ws(p);
if(*p->cursor != '=') {
_hmml_err(p, "Expected '=', got [%.3s]", p->cursor);
}
++p->cursor;
char* end = _hmml_read_attr(p, val_memory, sizeof(val_memory), 0);
_hmml_debug("read kv [%s] = [%s]\n", key_memory, val_memory);
key->ptr = key_memory;
key->len = key_len;
val->ptr = val_memory;
val->len = end - val_memory;
}
static HMML_Marker _hmml_parse_marker(struct _hmml_parser* p)
{
static char marker_mem[4096];
// the extended markers are inside [ ] and can contain parameters
_Bool extended = *p->cursor == '[';
if(extended) {
++p->cursor;
}
HMML_Marker marker = {
.offset = -1,
};
char c = *p->cursor;
if(c == '~') {
marker.type = HMML_PROJECT;
} else if(c == '@') {
marker.type = HMML_MEMBER;
} else if(c == ':') {
marker.type = HMML_CATEGORY;
} else {
_hmml_err(p, "Unknown marker type");
}
++p->cursor;
char* end = _hmml_read_attr(p, marker_mem, sizeof(marker_mem), !extended);
marker.marker = _hmml_persist_str(p, (struct _hmml_str){ marker_mem, end - marker_mem });
if(extended) {
_hmml_skip_ws(p);
if(*p->cursor == '#') {
++p->cursor;
size_t n = strcspn(p->cursor, " ");
marker.episode = _hmml_persist_str(p, (struct _hmml_str){ p->cursor, n });
p->cursor += n + 1;
}
if(*p->cursor != ']') {
const char* end = p->cursor;
for(;;) {
if(!*end) {
break;
}
char converted;
if(*end == '\\' && _hmml_unesc(end[1], &converted)) {
end += 2;
} else if(*end == ']'){
break;
} else {
++end;
}
}
marker.parameter = _hmml_persist_str(p, (struct _hmml_str){ p->cursor, end - p->cursor });
p->cursor = end;
}
if(*p->cursor != ']') {
_hmml_err(p, "Expected ']'");
}
++p->cursor;
}
return marker;
}
static HMML_Reference _hmml_parse_ref(struct _hmml_parser* p)
{
HMML_Reference ref = {
.offset = -1,
};
struct str_attr {
struct _hmml_str str;
char** dest;
} str_attrs[] = {
{ HSTR("site") , &ref.site },
{ HSTR("page") , &ref.page },
{ HSTR("url") , &ref.url },
{ HSTR("title") , &ref.title },
{ HSTR("article") , &ref.article },
{ HSTR("author") , &ref.author },
{ HSTR("editor") , &ref.editor },
{ HSTR("publisher"), &ref.publisher },
{ HSTR("isbn") , &ref.isbn },
};
for(;;) {
next_attr:
_hmml_skip_ws(p);
if(*p->cursor == ']') {
++p->cursor;
break;
}
struct _hmml_str key, value;
_hmml_read_kv(p, &key, &value);
for(size_t i = 0; i < countof(str_attrs); ++i) {
struct str_attr* s = str_attrs + i;
if(_hmml_str_eq(key, s->str)) {
*s->dest = _hmml_persist_str(p, value);
goto next_attr;
}
}
_hmml_err(p, "Unknown reference attribute");
}
return ref;
}
static void _hmml_parse_timecode(struct _hmml_parser* p, HMML_Timestamp* ts)
{
unsigned int h = 0, m = 0, s = 0, ms = 0;
int offset = 0;
int count = sscanf(p->cursor, "[%u:%u%n", &m, &s, &offset);
if(count < 2) {
_hmml_err(p, "Unable to parse timecode");
}
p->cursor += offset;
char c = *p->cursor;
if(c == ':') {
unsigned int tmp;
offset = 0;
if(sscanf(p->cursor, ":%u%n", &tmp, &offset) != 1 || offset == 0) {
_hmml_err(p, "Unable to parse 3-part timecode");
}
h = m;
m = s;
s = tmp;
p->cursor += offset;
c = *p->cursor;
}
if(c == '.') {
unsigned int tmp;
offset = 0;
int non_number_chars = 2;
int digits_in_100 = 3;
int max_chars_to_parse = non_number_chars + digits_in_100;
if(sscanf(p->cursor, ".%u]%n", &tmp, &offset) != 1 || offset == 0 || offset > max_chars_to_parse) {
_hmml_err(p, "Unable to parse %u.5-part timecode", h ? 3 : 2);
}
for(int i = offset - non_number_chars; i < digits_in_100; ++i) {
tmp *= 10;
}
ms = tmp;
p->cursor += offset;
} else if(c != ']') {
_hmml_err(p, "Unable to parse timecode");
} else {
++p->cursor;
}
if(ms >= 1000) {
_hmml_err(p, "Milliseconds cannot exceed 999");
}
if(s >= 60) {
_hmml_err(p, "Seconds cannot exceed 59");
}
if(m >= 60) {
_hmml_err(p, "Minutes cannot exceed 59");
}
ts->h = h;
ts->m = m;
ts->s = s;
ts->ms = ms;
}
static void _hmml_store_marker(struct _hmml_parser* p, HMML_Timestamp* ts, char** out, char* text_mem, size_t text_mem_size)
{
HMML_Marker m = _hmml_parse_marker(p);
m.offset = (*out) - text_mem;
_hmml_persist_array(p, &ts->markers, &ts->marker_count, m);
const char* marker_text = m.parameter
? m.parameter
: m.marker
;
size_t text_len = strlen(marker_text);
if((*out) + text_len > text_mem + text_mem_size) {\
_hmml_err(p, "Not enough text memory");\
}
memcpy(*out, marker_text, text_len);
*out += text_len;
}
static size_t _hmml_parse_text(struct _hmml_parser* p, HMML_Timestamp* ts)
{
static char text_mem[4096];
char* out = text_mem;
memset(text_mem, 0, sizeof(text_mem));
for(;;) {
size_t n = strcspn(p->cursor, "\\\n\r[]:@~");
char c = p->cursor[n];
if(out + n > text_mem + sizeof(text_mem)) {\
_hmml_err(p, "Not enough text memory");\
}
memcpy(out, p->cursor, n);
p->cursor += n;
out += n;
if(c == '\0') {
_hmml_err(p, "Unexpected EOF");
}
else if(c == ']') {
++p->cursor;
break;
}
else if(c == '\\') {
char converted;
if(_hmml_unesc(p->cursor[1], &converted)) {
*out++ = converted;
p->cursor += 2;
} else {
*out++ = '\\';
p->cursor++;
}
}
else if(c == '\n' || c == '\r') {
++p->cursor;
}
else if(c == '[') {
if(strncmp(p->cursor + 1, "ref", 3) == 0) {
p->cursor += 4;
HMML_Reference ref = _hmml_parse_ref(p);
ref.offset = out - text_mem;
_hmml_persist_array(p, &ts->references, &ts->reference_count, ref);
} else {
_hmml_store_marker(p, ts, &out, text_mem, sizeof(text_mem));
}
}
// it is a @ ~ or : marker without parameters
else {
// if next char is a space, or prev char is not a space*, then it can't be a marker
// * unless it's the first char
if(strchr(" \t\r\n", p->cursor[1]) || !(out == text_mem || strchr(" \t\r\n", p->cursor[-1]))) {
*out++ = c;
++p->cursor;
} else {
_hmml_store_marker(p, ts, &out, text_mem, sizeof(text_mem));
}
}
if((size_t)(out - text_mem) >= sizeof(text_mem)) {
_hmml_err(p, "Not enough text memory");
}
}
// trim trailing whitespace
while(out > text_mem && (uint8_t)(out[-1]) <= ' ') {
out[-1] = '\0';
--out;
}
size_t text_size = out - text_mem;
ts->text = _hmml_persist_str(p, (struct _hmml_str){ text_mem, text_size });
return text_size;
}
static void _hmml_parse_quote(struct _hmml_parser* p, HMML_Timestamp* ts)
{
char member[256];
int id;
int off = 0;
if(sscanf(p->cursor, "[quote %255s %d]%n", member, &id, &off) == 2 && off) {
ts->quote.present = 1;
ts->quote.id = id;
ts->quote.author = _hmml_persist_str(p, (struct _hmml_str){ member, strlen(member) });
} else if(sscanf(p->cursor, "[quote %d]%n", &id, &off) == 1 && off) {
ts->quote.present = 1;
ts->quote.id = id;
} else {
_hmml_err(p, "Unable to parse quote");
}
p->cursor += off;
}
static void _hmml_parse_timestamps(struct _hmml_parser* p)
{
for(;;) {
_hmml_skip_ws(p);
if(*p->cursor == '\0') {
_hmml_err(p, "Unexpected EOF");
}
if(strncmp(p->cursor, "[/video]", 8) == 0) {
break;
}
HMML_Timestamp ts = {
.line = p->line
};
_hmml_parse_timecode(p, &ts);
if(*p->cursor != '[') {
_hmml_err(p, "Expected '['");
}
if(p->cursor[1] == '@') {
HMML_Marker m = _hmml_parse_marker(p);
ts.author = m.marker;
}
++p->cursor;
int text_len = _hmml_parse_text(p, &ts);
if(p->cursor[0] == '[' && p->cursor[1] == ':') {
++p->cursor;
do {
HMML_Marker m = _hmml_parse_marker(p);
_hmml_persist_array(p, &ts.markers, &ts.marker_count, m);
_hmml_skip_ws(p);
if(*p->cursor != ':' && *p->cursor != ']') {
_hmml_err(p, "Unterminated post-text category node");
}
} while(*p->cursor == ':');
++p->cursor;
}
if(p->cursor[0] == '[' && p->cursor[1] == 'q') {
_hmml_parse_quote(p, &ts);
}
// convert all markers to lowercase, fix any out of range offsets
for(size_t i = 0; i < ts.marker_count; ++i) {
HMML_Marker* m = ts.markers + i;
for(char* c = m->marker; *c; ++c) {
if(*c >= 'A' && *c <= 'Z') {
*c = (*c - ('A' - 'a'));
}
}
if(m->offset > text_len) {
m->offset = text_len;
}
}
for(size_t i = 0; i < ts.reference_count; ++i) {
HMML_Reference* ref = ts.references + i;
if(ref->offset > text_len) {
ref->offset = text_len;
}
}
_hmml_persist_array(p, &p->out.timestamps, &p->out.timestamp_count, ts);
}
}
static HMML_Credit _hmml_parse_credit(struct _hmml_parser* p, struct _hmml_str value)
{
HMML_Credit credit = {};
char* colon = strchr(value.ptr, ':');
if(colon) {
*colon = '\0';
credit.name = _hmml_persist_str(p, (struct _hmml_str){ value.ptr, colon - value.ptr });
credit.role = _hmml_persist_str(p, (struct _hmml_str){ colon+1, value.len - ((colon+1) - value.ptr) });
} else {
credit.name = _hmml_persist_str(p, value);
}
return credit;
}
static void _hmml_parse_video(struct _hmml_parser* p)
{
struct str_attr {
struct _hmml_str str;
char** dest;
} str_attrs[] = {
{ HSTR("stream_platform"), &p->out.metadata.stream_platform },
{ HSTR("project") , &p->out.metadata.project },
{ HSTR("title") , &p->out.metadata.title },
{ HSTR("vod_platform") , &p->out.metadata.vod_platform },
{ HSTR("id") , &p->out.metadata.id },
{ HSTR("template") , &p->out.metadata.template },
{ HSTR("medium") , &p->out.metadata.medium },
{ HSTR("number") , &p->out.metadata.number },
{ HSTR("output") , &p->out.metadata.output },
{ HSTR("cc_lang") , &p->out.metadata.cc_lang },
};
for(;;) {
next_attr:
_hmml_skip_ws(p);
if(*p->cursor == ']') {
++p->cursor;
_hmml_parse_timestamps(p);
return;
}
struct _hmml_str key, value;
_hmml_read_kv(p, &key, &value);
for(size_t i = 0; i < countof(str_attrs); ++i) {
struct str_attr* s = str_attrs + i;
if(_hmml_str_eq(key, s->str)) {
*s->dest = _hmml_persist_str(p, value);
goto next_attr;
}
}
if(_hmml_str_eq(key, HSTR("credit"))) {
HMML_Credit credit = _hmml_parse_credit(p, value);
_hmml_persist_array(p, &p->out.metadata.credits, &p->out.metadata.credit_count, credit);
goto next_attr;
}
if(_hmml_str_eq(key, HSTR("uncredit"))) {
HMML_Credit uncredit = _hmml_parse_credit(p, value);
_hmml_persist_array(p, &p->out.metadata.uncredits, &p->out.metadata.uncredit_count, uncredit);
goto next_attr;
}
HMML_VideoCustomMetaData custom = {
.key = _hmml_persist_str(p, key),
.value = _hmml_persist_str(p, value),
};
_hmml_persist_array(p, &p->out.metadata.custom, &p->out.metadata.custom_count, custom);
}
}
HMML_Output hmml_parse(const char* string)
{
struct _hmml_parser p = {
.mem = string,
.cursor = string,
.line = 1,
};
if(setjmp(p.err_buf) == 1) {
// if it returns 1, an error happened
p.out.free_list = p.free_list;
return p.out;
}
const struct _hmml_str prefix = HSTR("[video");
if(strncmp(p.cursor, prefix.ptr, prefix.len)) {
_hmml_err(&p, "Missing initial video tag.");
} else {
p.cursor += prefix.len;
_hmml_parse_video(&p);
}
p.out.free_list = p.free_list;
p.out.well_formed = 1;
return p.out;
}
void hmml_free(HMML_Output* out)
{
if(!out->free_list) {
return;
}
for(uintptr_t i = 2; i < ((uintptr_t*)out->free_list)[1]; ++i) {
free(((void**)out->free_list)[i]);
}
free(out->free_list);
}
const struct HMML_Version hmml_version = {
2, 0, 15
};
#undef HSTX
#undef HSTR
#endif

2
hmmlib2/utils/Makefile Normal file
View File

@ -0,0 +1,2 @@
hmmldump: dump.c ../hmmlib.h stb_sb.h
gcc -Wall -Wextra -I.. -g $< -o $@

241
hmmlib2/utils/dump.c Normal file
View File

@ -0,0 +1,241 @@
#define HMMLIB_IMPLEMENTATION
#include "hmmlib.h"
#include "stb_sb.h"
#include <stdbool.h>
#include <getopt.h>
typedef struct {
char* text;
int* lines;
} Index;
static Index* index_find(Index* base, const char* text)
{
for(size_t i = 0; i < sb_count(base); ++i){
if(strcmp(base[i].text, text) == 0){
return base + i;
}
}
return NULL;
}
void hmml_dump(HMML_Output* hmml, bool extra)
{
if(!hmml){
puts("(null)");
return;
}
if(!hmml->well_formed){
printf("Error:%d:%d %s\n", hmml->error.line, hmml->error.col, hmml->error.message);
return;
}
if(extra) {
puts("Metadata:");
static const char* meta[] = { "stream_platform", "project", "title", "vod_platform", "id", "output", "template", "medium" };
for(size_t i = 0; i < countof(meta); ++i) {
const char* value = ((char**)&hmml->metadata)[i];
printf(" %s = %s\n", meta[i], value);
}
puts(" Credits:");
for(size_t i = 0; i < hmml->metadata.credit_count; ++i) {
HMML_Credit* c = hmml->metadata.credits + i;
printf(" %s [%s]\n", c->name, c->role);
}
puts(" Custom:");
for(size_t i = 0; i < hmml->metadata.custom_count; ++i) {
HMML_VideoCustomMetaData* m = hmml->metadata.custom + i;
printf(" %s = %s\n", m->key, m->value);
}
}
puts("Annotations:");
for(size_t i = 0; i < hmml->annotation_count; ++i){
HMML_Annotation* a = hmml->annotations + i;
char time_buf[256];
char* tp = time_buf;
if(a->h) {
*tp++ = (a->h%10) + '0';
sprintf(tp, ":%02d:%02d", a->m, a->s);
} else {
sprintf(tp, " %2d:%02d", a->m, a->s);
}
printf("\t%3d [%s] [%s]\n", a->line, time_buf, a->text);
}
Index* authors = NULL;
Index* markers[HMML_MARKER_COUNT] = {};
int max_text_len = 0;
for(size_t i = 0; i < hmml->annotation_count; ++i){
HMML_Annotation* a = hmml->annotations + i;
if(a->author){
int len = strlen(a->author);
if(len > max_text_len){
max_text_len = len;
}
Index* idx;
if(!(idx = index_find(authors, a->author))){
Index x = { .text = a->author };
sb_push(authors, x);
idx = &sb_last(authors);
}
sb_push(idx->lines, a->line);
}
}
for(size_t i = 0; i < hmml->annotation_count; ++i){
HMML_Annotation* a = hmml->annotations + i;
for(size_t j = 0; j < a->marker_count; ++j){
int type = a->markers[j].type;
char* text = a->markers[j].marker;
int len = strlen(text);
if(len > max_text_len){
max_text_len = len;
}
Index* idx;
if(!(idx = index_find(markers[type], text))){
Index x = { .text = text };
sb_push(markers[type], x);
idx = &sb_last(markers[type]);
}
sb_push(idx->lines, a->line);
}
}
puts("Authors:");
for(size_t i = 0; i < sb_count(authors); ++i){
printf("\t %*s: ", max_text_len, authors[i].text);
for(size_t j = 0; j < sb_count(authors[i].lines); ++j){
printf("%3d ", authors[i].lines[j]);
}
puts("");
}
static const char* m_tags[HMML_MARKER_COUNT] = { "Categories", "Members", "Projects" };
for(size_t i = 0; i < HMML_MARKER_COUNT; ++i){
printf("%s:\n", m_tags[i]);
for(size_t j = 0; j < sb_count(markers[i]); ++j){
printf("\t %*s: ", max_text_len, markers[i][j].text);
for(size_t k = 0; k < sb_count(markers[i][j].lines); ++k){
printf("%3d ", markers[i][j].lines[k]);
}
puts("");
}
}
static const char* r_tags[] = { "Site", "Page", "URL", "Title", "Article", "Author", "Editor", "Publisher", "ISBN" };
puts("References:");
for(size_t i = 0; i < hmml->annotation_count; ++i){
HMML_Annotation* a = hmml->annotations + i;
for(size_t j = 0; j < a->reference_count; ++j){
printf("\t%3d ", a->line);
HMML_Reference* r = a->references + j;
for(size_t k = 0; k < countof(r_tags); ++k){
char* item = ((char**)r)[k];
if(item){
printf("[%s = %s] ", r_tags[k], item);
}
}
puts("");
}
}
puts("Quotes:");
for(size_t i = 0; i < hmml->annotation_count; ++i){
HMML_Annotation* a = hmml->annotations + i;
if(a->quote.present){
if(a->quote.author){
printf("\t%3d [Quote #%d, by %s]", a->line, a->quote.id, a->quote.author);
} else {
printf("\t%3d [Quote #%d]", a->line, a->quote.id);
}
puts("");
}
}
for(size_t i = 0; i < sb_count(authors); ++i){
sb_free(authors[i].lines);
}
sb_free(authors);
for(size_t i = 0; i < HMML_MARKER_COUNT; ++i){
for(size_t j = 0; j < sb_count(markers[i]); ++j){
sb_free(markers[i][j].lines);
}
sb_free(markers[i]);
}
}
void usage(char* argv0, FILE* out)
{
fprintf(out, "Usage: %s [-bx] [file]\n", argv0);
}
int main(int argc, char** argv)
{
bool dump_extra = false;
bool breakpoint = false;
int opt;
while((opt = getopt(argc, argv, "bx")) != -1) {
if(opt == 'x') {
dump_extra = true;
} else if(opt == 'b') {
breakpoint = true;
} else {
usage(argv[0], stderr);
return 1;
}
}
if(optind >= argc){
usage(argv[0], stderr);
return 1;
}
argc -= (optind-1);
argv += (optind-1);
FILE* f = fopen(argv[1], "r");
if(!f) {
perror(argv[1]);
return 1;
}
fseek(f, 0, SEEK_END);
long size = ftell(f);
rewind(f);
char* mem = malloc(size+1);
mem[size] = 0;
fread(mem, 1, size, f);
fclose(f);
HMML_Output out = hmml_parse(mem);
free(mem);
if(breakpoint) {
asm("int3");
}
hmml_dump(&out, dump_extra);
hmml_free(&out);
}

View File

@ -0,0 +1,16 @@
fuzz: fuzz.c ../../hmmlib.h
afl-gcc -I../.. -g -D_GNU_SOURCE -fprofile-arcs -ftest-coverage $< -o $@
run: fuzz | output
afl-fuzz -i tests -o output ./$<
cov: fuzz | output
afl-cov -d output --coverage-cmd 'cat AFL_FILE | ./fuzz' --code-dir . --enable-branch-coverage --overwrite
output:
mkdir -p $@
clean:
rm -rf fuzz fuzz.gcno fuzz.gcda output
.PHONY: run cov clean

20
hmmlib2/utils/fuzz/fuzz.c Normal file
View File

@ -0,0 +1,20 @@
#define HMMLIB_IMPLEMENTATION
#include "hmmlib.h"
int main(int argc, char** argv)
{
char* mem = NULL;
size_t size = 0;
char buf[BUFSIZ];
while(fgets(buf, BUFSIZ, stdin)) {
size_t n = strlen(buf) + 1;
mem = realloc(mem, size + n);
memcpy(mem + size, buf, n);
size += n - 1;
}
HMML_Output out = hmml_parse(mem);
free(mem);
hmml_free(&out);
}

View File

@ -0,0 +1,11 @@
[video member=nothings credit="inso:programmer" annotator=Miblo]
[2:01][Recap and update the TODO list]
[38:58][Continue implementing parse_tag()][:parsing :test]
[41:40][@miblo][Ohhh, right. Yeah, the \[video\] and \[/video\] tags are the only :ones that [:have a s d f] that open-close format. All the [~other] [@tags abc] are "single" tags]
[45:17][Enable parse_tag() to tokenise the \[video\] node][:parsing][quote bob 1]
[3:42:11][@miblo][@nothings2: That's fine, yeah! They are also in: [ref
site="GitLab: Annotation-Pushers / Annotation-Game"
page="projects/nothings/obbg"
url=http://git.handmadedev.org/Annotation-Pushers/Annotation-Game/tree/master/projects/nothings/obbg]]
[3:50:44][Take a break][quote 5]
[/video]

View File

@ -0,0 +1,3 @@
[video credit=inso:programmer]
[2:01][Recap and :update the ~TODO @list]
[/video]

View File

@ -0,0 +1,25 @@
[video member=nothings stream_platform=twitch stream_username=nothings2 project=obbg title="Open Block Building Game Development #32 (1/2)" vod_platform=youtube id=Vm-0ZUVMHHc annotator=Miblo]
[2:01][Recap and update the TODO list]
[10:08][Consult the example YouTube description file and the longest .hmml file][:test1 :test2 :abcd]
[38:58][Continue implementing parse_tag()][:parsing]
[41:40][@miblo][Ohhh, right. Yeah, the \[video\] and \[/video\] tags are the only ones that have that open-close format. All the other tags are "single" tags]
[41:51][Continue writing parse_tag() anyway][:parsing]
[43:10][@miblo][@nothings2: There isn't really, other than the rambling in: [ref
site="GitLab: Annotation-Pushers / Annotation-System / Issues"
page="Handmade Annotation Markup Language (previously MibloMarkup)"
url=http://git.handmadedev.org/Annotation-Pushers/Annotation-System/issues/2]]
[45:17][Enable parse_tag() to tokenise the \[video\] node][:parsing]
[1:51:32][Parse out the embedded \: and \@ tags][:parsing]
[1:57:50][:Run it to see if those two cases work and tweak the username conversion]
[3:15:14][@experior][Add a space after the pound? Or another character]
[3:16:40][Delete the \@ in the text node][:parsing]
[3:42:11][@miblo][@nothings2: That's fine, yeah! They are also in: [ref
site="GitLab: Annotation-Pushers / Annotation-Game"
page="projects/nothings/obbg"
url=http://git.handmadedev.org/Annotation-Pushers/Annotation-Game/tree/master/projects/nothings/obbg]]
[3:47:14][@insofaras][[ref
site="GitHub: nothings/obbg"
page="Pull Request #8: get it building and running on linux by insofaras"
url=https://github.com/nothings/obbg/pull/8/files#diff-90c561ba68b3be193f3378639ef489a0R1831]]
[3:50:44][Take a break]
[/video]

59
hmmlib2/utils/stb_sb.h Normal file
View File

@ -0,0 +1,59 @@
// stb stretchy_buffer.h v1.02 nothings.org/stb
// with custom addtions sb_end, sb_pop, sb_erase
#ifndef STB_STRETCHY_BUFFER_H_INCLUDED
#define STB_STRETCHY_BUFFER_H_INCLUDED
#ifndef NO_STRETCHY_BUFFER_SHORT_NAMES
#define sb_free stb_sb_free
#define sb_push stb_sb_push
#define sb_count stb_sb_count
#define sb_add stb_sb_add
#define sb_last stb_sb_last
#define sb_end stb_sb_end
#define sb_pop stb_sb_pop
#define sb_erase stb_sb_erase
#define sb_each stb_sb_each
#endif
#define stb_sb_free(a) ((a) ? free(stb__sbraw(a)),(a)=0,0 : 0)
#define stb_sb_push(a,v) (stb__sbmaybegrow(a,1), (a)[stb__sbn(a)++] = (v))
#define stb_sb_count(a) ((a) ? stb__sbn(a) : 0)
#define stb_sb_add(a,n) (stb__sbmaybegrow(a,n), stb__sbn(a)+=(n), &(a)[stb__sbn(a)-(n)])
#define stb_sb_last(a) ((a)[stb__sbn(a)-1])
#define stb_sb_end(a) ((a) ? (a) + stb__sbn(a) : 0)
#define stb_sb_pop(a) (--stb__sbn(a))
#define stb_sb_erase(a,i) ((a) ? memmove((a)+(i), (a)+(i)+1, sizeof(*(a))*((--stb__sbn(a))-(i))),0 : 0);
#define stb__sbraw(a) ((size_t *) (a) - 2)
#define stb__sbm(a) stb__sbraw(a)[0]
#define stb__sbn(a) stb__sbraw(a)[1]
#define stb__sbneedgrow(a,n) ((a)==0 || stb__sbn(a)+(n) >= stb__sbm(a))
#define stb__sbmaybegrow(a,n) (stb__sbneedgrow(a,(n)) ? stb__sbgrow(a,n) : 0)
#define stb__sbgrow(a,n) ((a) = stb__sbgrowf((a), (n), sizeof(*(a))))
#define stb_sb_each(n,h) for(typeof(h) n = h; n < sb_end(h); ++n)
#include <stdlib.h>
static inline void * stb__sbgrowf(void *arr, int increment, int itemsize)
{
size_t inc_cur = arr ? stb__sbm(arr) + (stb__sbm(arr) >> 1) : 0;
size_t min_needed = stb_sb_count(arr) + increment;
size_t m = inc_cur > min_needed ? inc_cur : min_needed;
size_t *p = (size_t *) realloc(arr ? stb__sbraw(arr) : 0, itemsize * m + sizeof(size_t)*2);
if (p) {
if (!arr)
p[1] = 0;
p[0] = m;
return p+2;
} else {
#ifdef STRETCHY_BUFFER_OUT_OF_MEMORY
STRETCHY_BUFFER_OUT_OF_MEMORY ;
#endif
return (void *) (2*sizeof(size_t)); // try to force a NULL pointer exception later
}
}
#endif // STB_STRETCHY_BUFFER_H_INCLUDED